حدس: بعد از هر جواب، ۳ تا ۵ کاری که حبیب احتمالاً بعدش میخواهد را بالای کادر دکمه میکند

A simple script to add RTL (Right-to-Left) text support to the Claude Code extension in VS Code-based IDEs.
# Clone the repository
git clone https://github.com/YOUR_USERNAME/claude-code-extension-rtl.git
cd claude-code-extension-rtl
# Make the script executable
chmod +x fix-rtl-claude.sh
# Run the script
./fix-rtl-claude.sh
# Basic RTL support (no custom font)
./fix-rtl-claude.sh
# With Vazirmatn font (recommended for Persian/Arabic)
./fix-rtl-claude.sh --with-font
# Undo everything: restore every patched file from its .backup
./fix-rtl-claude.sh --revert
# Reload the frontmost IDE window when done (default on an interactive run)
./fix-rtl-claude.sh --reload
./fix-rtl-claude.sh --no-reload
# Show help
./fix-rtl-claude.sh --help
Default sizes can be overridden per run:
CHAT_FONT_SIZE=17px USER_MSG_LINES=2 ./fix-rtl-claude.sh
| Variable | Meaning | Default |
|---|---|---|
CHAT_FONT_SIZE | conversation text | 15px |
CHAT_LINE_HEIGHT | conversation line height | 1.8 |
CODE_FONT_SIZE | code blocks and tables | 12px |
USER_MSG_LINES | lines of your own messages (0 = full) | 1 |
SIDE_BTN_FONT | quick-command buttons | 9px |
CHROME_FONT_SIZE | header, footer, labels | 10px |
# Clone the repository
git clone https://github.com/YOUR_USERNAME/claude-code-extension-rtl.git
cd claude-code-extension-rtl
# Run the script
.\fix-rtl-claude.ps1
# Basic RTL support (no custom font)
.\fix-rtl-claude.ps1
# With Vazirmatn font (recommended for Persian/Arabic)
.\fix-rtl-claude.ps1 -WithFont
# Show help
.\fix-rtl-claude.ps1 -Help
Note: If you get an execution policy error, run PowerShell as Administrator and execute:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
The extension updates may overwrite the CSS changes. Simply run the script again after each update:
macOS/Linux:
./fix-rtl-claude.sh
Windows:
.\fix-rtl-claude.ps1
fix-rtl-claude.sh turns off claudeCode.scrollToBottomOnSend (extension 2.1.276+) in the settings.json of VSCode, Cursor, Windsurf and Devin, through ensure-scroll-setting.py. Reading higher up, pressing Enter or a streaming reply no longer drags the page down; scroll to the bottom yourself and it follows new text again. The key is only added when missing — set it to true and the script leaves it alone. On a second machine: git status (must be clean), then git pull && ./auto-update-claude-code.sh, then reload the IDE.
aA)The patch injects a small settings panel into the chat panel itself, so sizes can be tuned live without touching any file. Click aA at the bottom edge.
| Control | What it does |
|---|---|
| فشرده / معمولی / درشت | one-click presets |
| متن گفتگو | conversation font size (slider + number box) |
| کد و جدول | code block and table font size |
| حواشی | header, footer, labels |
| دکمهها | the quick-command buttons themselves |
| پیام خودت | collapse your own messages to 1, 2, 3 lines or show them in full — hover or click to expand |
| جای دکمهها | both edges / all left / all right |
| جمع | shrink and dim Thinking and tool-call blocks until hovered |
| جدول | horizontal scroll for wide tables and code instead of stretching the page |
| دکمهها: مخفی | hide the button columns — shortcut Ctrl+Alt+B |
| لیست دکمهها (JSON) | add, remove or rename the quick-command buttons in place |
| شفافیت / رنگ | button opacity and an accent colour (empty = follow the theme) |
| پروفایل | save the current settings under a name and switch between them |
| جستوجو | find text in the conversation and step through hits — Ctrl+Alt+F |
| کپی گفتگو | copy the whole conversation as Markdown |
| متنهای ویژه | a list of ready texts, one line each: ☑ tick which ones are glued on, «اول/آخر» per text, 📋 per text = also on the copy button under each reply, ↑↓ order (higher goes first), ✎ edit, ✕ twice to delete, «+ افزودن». The first right-click item «کپی با الصاق متن ویژه» uses every ticked text; «کپی تنظیمات» carries the list to other IDEs; an old single text is migrated in |
| کپی همه | copy this chat's identity in one go; the first line is a ready SendMessage to: "<title> [ref]" that an agent on another device can use over Remote Control. The ref is sha256("bridge-session:<bridgeSessionId>")[:6] and survives a resume; with Remote Control off it says so instead of a code |
sel | push the editor's current selection into the chat |
| شمارنده | character count of the composer, plus the app's own usage line when it renders one |
sav | copy the current settings as JSON for ~/.claude-rtl-sizes.json |
rst | back to defaults |
A folded Thinking or tool-call block opens on hover, and a click pins it open.
Select any text in the conversation and right-click it:
| Menu entry | What it does |
|---|---|
| ↩︎ نقلقول در چت | drops the selection into the composer as a > quote |
| ✎ بدون علامت نقلقول | same, without the quote marker |
| ⧉ کپی | plain copy |
| 🔍 جستوجوی همین متن | opens the find bar preloaded with the selection |
| ؟ بپرس دربارهٔ… | quotes it and starts a question about that passage |
Ctrl+Alt+Q quotes the selection without opening the menu. The menu only appears over conversation text — the composer keeps VSCode's own context menu, and so does a right-click with nothing selected. Toggle it from the panel.
Drag a column by its ⋮⋮ grip to move it anywhere on screen.
Settings live in the webview's localStorage. To make them the defaults for every IDE and every extension update, press sav and paste the JSON into ~/.claude-rtl-sizes.json — the script seeds the panel from that file. See sizes.example.json for the shape. Keep that file in your dotfiles and symlink it to carry the same setup across machines:
ln -sf ~/dotfiles/claude-rtl-sizes.json ~/.claude-rtl-sizes.json
The script patches the Claude Code extension webview with RTL-specific CSS:
direction: rtl for text elementsThe CSS goes into webview/index.css and into the inline <style> that extension.js builds for the webview HTML. The second copy matters — the webview resource URLs carry no cache-buster, so a plain window reload keeps serving the stale index.css, while the HTML is rebuilt on every panel creation and is therefore never cached.
The settings-panel script rides along in that same HTML, as an inline <script nonce=...>, and only there: appending it to webview/index.js as well would run the whole thing twice in one webview. index.js is only used as a fallback when python3 is missing or its rewrite fails, in which case the run warns and carries on instead of dying half-patched.
Since 2.1.267 the message box is stacked: .messageInput_* holds the real text but paints it transparent (color:#0000, only the caret shows), and .mentionMirror_* is an absolutely positioned overlay that paints what you actually see, mention chips included.
So the text you see and the text you select live in different elements. Any property set on one must be set identically on the other — font, size, line-height, letter-spacing, padding, direction, bidi — or the two drift and selections land in the wrong place. An earlier version of this patch shrank inputMentionChip_* to 9px because the class contains "Chip", which broke exactly that.
The rule this patch follows: do not style the composer subtree at all. Every selector here excludes [class*="messageInputContainer_"] *, and the Vazirmatn override hands that subtree back to var(--vscode-chat-font-family). 2.1.267 also gave the composer unicode-bidi:plaintext — each line takes its direction from its own first strong character — which is the correct RTL behaviour and must not be overridden.
Press aA → تست in the panel to print a computed-style comparison of the two layers; every row should read ok.
--with-fontcom.habib.fix-rtl-claude.plist runs the script with --with-font, so the CSS that actually lands on the machine is always the Vazirmatn variant. A plain ./fix-rtl-claude.sh run produces different CSS and will be overwritten the next time the agent fires. Test with ./fix-rtl-claude.sh --with-font so you are looking at what the agent will produce.
Backups are created automatically the first time (index.css.backup, index.js.backup, extension.js.backup) and every run re-patches from those backups, so repeated runs never stack up. --revert restores them.
Each patched folder gets a .crtl-stamp holding a hash of the injected payload — CSS, the buttons script, and ~/.claude-rtl-sizes.json, so editing the seed file is enough to trigger a re-patch. A run whose output would be byte-identical exits silently and writes nothing to the log — the launchd agent fires on every touch of the extensions folder, and re-patching each time was the only thing growing autofix.log. --force patches anyway.
After patching, the script runs node --check on both injected files and confirms the panel marker is present. A file that fails is rolled back from its backup rather than left broken.
No Claude Code extensions found
~/.<ide-name>/extensions/anthropic.claude-code-*/webview/%USERPROFILE%\.<ide-name>\extensions\anthropic.claude-code-*\webview\Changes not visible
Cmd+R (Reload Window). The inline injection survives the webview cache, so a reload is enough — a full restart is not needed.rm -rf ~/Library/Application\ Support/Code/Cache/* ~/Library/Application\ Support/Code/Code\ Cache/*The patch disappeared after an extension update
[WARN] line for any version it finds unpatched.PowerShell execution policy error (Windows)
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserpowershell -ExecutionPolicy Bypass -File .\fix-rtl-claude.ps1This is a known bug in Claude Code itself (not related to this RTL fix):
To kill runaway processes:
# interactive: lists anything above 80% CPU, then asks
./kill-claude-zombies.sh
# unattended, higher bar
./kill-claude-zombies.sh --yes --threshold 90
# everything, no threshold
./kill-claude-zombies.sh --all
--threshold insists on a number: a missing or non-numeric value used to make awk compare against an empty string and match every process — a silent --all. It now exits with an error instead.
To reap them automatically every 10 minutes:
cp com.habib.kill-claude-zombies.plist ~/Library/LaunchAgents/
launchctl load ~/Library/LaunchAgents/com.habib.kill-claude-zombies.plist
The patcher can do it in the same pass with ./fix-rtl-claude.sh --kill-zombies.
MIT License - See LICENSE file for details.
Contributions are welcome! Feel free to open issues or submit pull requests.
fix-rtl-chatgpt.sh patches installed openai.chatgpt-* extensions separately from Claude. Requires Python 3 and Node.js on macOS/Linux. It discovers the same six IDE extension directories as the Claude patcher.
./fix-rtl-chatgpt.sh --with-font
# macOS: reapply every five minutes, including after extension updates
python3 install-chatgpt-autofix.py
Run Developer: Reload Window in VS Code after installation. The aA panel provides text/code sizes, line spacing, colours, draggable command buttons, profiles, user-message folding, search, copy, quoting and extra copy texts. Buttons insert drafts; the default buttons do not send messages. Settings use a separate chatgpt-rtl-sizes localStorage key and optional ~/.chatgpt-rtl-sizes.json seed. --with-font uses Vazirmatn if installed, otherwise Tahoma/sans-serif; no remote font requests or CSP changes are needed.
The adapter builds from claude-ui-buttons.js, mapping transcript selectors to ChatGPT's installed DOM. It excludes Claude's host/session/Remote Control, editor-selection and glossary integration, Claude slash commands, chrome sizing and tool-call folding. Native composer styling and native tool approvals are preserved. It does not change extension update settings or manage processes. Conversation copy/search cover rendered transcript content, which may exclude older virtualized turns. Future DOM changes may require updating the adapter.
The HTML has an original backup; generated JS/CSS filenames carry a content hash for cache invalidation. The patch validates JS before installation and refuses to overwrite HTML changed by another process. Background runs never reload the IDE. To undo, remove the background agent first:
python3 install-chatgpt-autofix.py --remove
./fix-rtl-chatgpt.sh --revert
Unused generated assets remain inert after revert so open webviews can finish loading. For an explicit extension location, pass --extension-dir /path/to/openai.chatgpt-version.
Validation: python3 -m unittest test_chatgpt_patch.py checks repeatability, exact HTML restoration, external-change protection and invalid-JS refusal. Browser fixture QA checks panel creation, RTL prose/LTR code, native composer direction, quick-button draft insertion and settings persistence. A reload and live visual check in the user's VS Code window are still required.
The ChatGPT patch reserves a 26px top toolbar for quick buttons, aA and the native run-location selector. Composer minimum height is reduced; long drafts remain scrollable and the native expanded-composer mode is preserved. Attachment previews are limited to a 76px scrolling area with 56px images. RTL lists use an explicit RTL base so English-leading items keep their bullets on the right. A full Developer: Reload Window rebuilds existing panels with the new assets; Reload Webviews can reuse the old host-generated HTML.
The patch also adds a compact tab row inside the Codex panel. + opens a native /extension/panel/new composer. There is one unsent draft per panel; pressing + again focuses it. Sending a message gives that chat its own tab. Synthetic client-new-thread routes are not used: the live Windsurf app treats them as pending worktree threads and disables sending. Opening an existing conversation adds it to the row; selecting a tab uses Codex's native router. Closing a tab only removes the shortcut, without deleting the conversation or stopping work. Double-click a tab to give it a short label. The tab list is stored in the webview's session storage; draft contents remain owned by Codex. Restoration across IDE shutdowns is not promised.
chatgpt-tabs.js discovers the outer React Router through the installed app's __codexRoot. It supports both the data RouterProvider in VS Code and the MemoryRouter navigator in Windsurf; it does not replace the native history listener. Query parameters are preserved when switching tabs. Leaving settings through the MemoryRouter fallback is refused so native unsaved-settings confirmation is not bypassed. This is an internal integration and can require maintenance after an extension update. If the router cannot be found, the add button stays disabled and an explanation is shown; host executable code and native bundled assets are not patched. test_chatgpt_tabs.cjs uses real React/React Router in an isolated fixture to test navigation, native draft entry, saved thread tabs, close behavior, failed navigation and deliberate routing breakage. Run with NODE_PATH=/tmp/chatgpt-rtl-qa/node_modules node test_chatgpt_tabs.cjs and again with TABS_ROUTER=memory for the Windsurf router. The latter also removes the MemoryRouter connection deliberately and verifies the disabled error state. Live IDE testing is separate.
Async question cards are scoped by data-codex-composer-request-navigation; their question text, options and custom answer field now have RTL direction. Code inside a question stays LTR. The main multiline composer wrapper's inline minimum-height variable is overridden directly, allowing one line when empty and growth for longer drafts. This does not apply to native expanded mode.
Rendering regression check (requires Playwright and Chrome): NODE_PATH=/tmp/chatgpt-rtl-qa/node_modules node test_chatgpt_layout.cjs. The isolated fixture checks option ordering, mixed-language paragraph isolation, code direction, empty composer height and long-draft growth. It deliberately breaks both the question direction and minimum-height rule and requires both regressions to be caught. This fixture is not a substitute for checking the live question card and composer after Developer: Reload Window.
Transcript block gaps use the native --conversation-item-gap at 6px instead of 16px. The elapsed-work divider keeps its time and disclosure control in a smaller row; thinking slots and transcript bottom padding are also compacted. Virtualized row heights/margins are not overridden, so native scroll measurement continues to use the rendered content. The layout test measures before/after, checks that code/table spacing and unrelated surfaces retain their dimensions, and deliberately enlarges the gap to confirm that the regression is caught.
Live Windsurf check (2026-10-18): the tab connection error disappeared and returning to the original conversation worked. The real new-composer entry redirected to Codex onboarding (Next); first-message sending has not been verified. Computer-control errors prevented completing that UI check.
The in-panel گفتگوی قدیمی button opens the native recent-chat picker in add-to-tabs mode. Local history row selections are routed inside this panel using the row identity and host ID, preserving existing tabs. Native nested actions such as Archive are not intercepted. The fixture tests host-panel opening suppression, old-tab preservation, remote host parameters, and a deliberately disconnected history handler. Live user verification is pending.
hooks/register.js 1318 lines1// حدس: «دو جمله» is Sonnet 5.5 on the last messages, after each answer and (braked) after
2// tools: cheap. The 3-5 guesses are one fork of the chat itself (its own model, the whole chat
3// from the warm cache) every hads_har_chand_javab answers; Sonnet is their fallback. Each guess is a
4// button above the prompt; a click fills the prompt box and never sends.
5export const model_do_jomle_va_hads = 'claude-sonnet-5-5'
6// Habib: «حدسو بذار مدل اصلی بکنه … منتها مثلاً چند تا پیام به چند تا پیام»
7export const hads_har_chand_javab = 3
8// answers seen per chat id since this process started: guesses are due on 1st, 4th, 7th…
9const tedad_javab_har_chat = new Map()
10
11const prompt_system_hads = [
12 'You predict what the user (حبیب) will ask next in a Claude Code session.',
13 'Give 1 or 2 items: what the user will most likely ask next, or an option the user has not',
14 'thought of but that fit the user\'s pattern. Each item is ready to send as the user\'s own message,',
15 'in plain Persian, in the user\'s tone. Reply ONLY with JSON: [{"label": "<= 3 words", "text": "<one line>"}].',
16].join(' ')
17
18let hads_akhar = []
19// «نکردهها» (Habib, 25 Oct 04:22: «چه کارایی حبیب بهت گفته و نکردی و مثل حدس دو تا توی هر دفعه
20// داده بشه»): what he asked in this chat that is still not done, two each time, made with the guesses
21let nakarde_akhar = []
22// the «نکرده» rows a check found done (its file or a commit says so): not in the list, but one small
23// line counts them and opens them, so a loose check never hides real work for good (۲۰۵, 25 Oct 19:17)
24let nakarde_ba_sanad_akhar = []
25let nakarde_ba_sanad_baz = false
26// «خودکار» (Habib, 25 Oct 16:48: «کارهایی که خود مدل میتونه انجام بده … جوری که مدل هی نخواد از من
27// بپرسه … به تعداد هیچ محدود نباشه»): of the work now running in this chat, what the model can do
28// by itself with its tools instead of asking Habib, and how; made with the guesses, in the same fork
29let khodkar_akhar = []
30let cron_akhar = []
31let hads_dar_kar = false
32// the chat the guesses were made in: after /clear (a new id) they are not shown
33let hads_baraye_chat = ''
34
35// «دو جمله»: what the agent is doing now, in Habib's words (the project's 00_vazhe_name.md),
36// after each answer and, at most every do_jomle_fasele_ms ms, after a tool. One call at a time.
37export const do_jomle_fasele_ms = 60000
38// over this many tokens of chat even a warm fork costs too much: Sonnet on the last messages
39export const had_fork_context_token = 400000
40// idle longer than this and the chat's cache is likely gone: «پرسش» warns first. The TTL is
41// 1 h (Claude Code says so: «Prompt cache warm, about 58 min left»); a model switch tells
42// the real one (classic.PostModelSwitch cache_ttl), a 5 min TTL makes it cache_sard_5m_ms.
43export const cache_sard_ms = 55 * 60000
44export const cache_sard_5m_ms = 4 * 60000
45let cache_sard_bad_az_ms = cache_sard_ms
46const prompt_system_do_jomle = [
47 'In exactly two short sentences of plain Persian, tell حبیب what the agent in this Claude Code',
48 'session is doing right now and what it is waiting for. Use the words of the glossary given, not',
49 'technical terms. Do not guess what comes next. Reply with the two sentences only.',
50].join(' ')
51let do_jomle_akhar = { text: '', for: '' }
52let do_jomle_dar_kar = false
53let do_jomle_dobare = false
54let zaman_do_jomle_akhar = 0
55// the end of an answer wins over a mid-work line still on its way
56let zaman_akhar_javab = 0
57let akhar_javab_dar_kar = false
58// an answer that ends while the last one is still told is told right after it, never dropped
59let akhar_javab_dobare = false
60// last time the chat itself talked to the model (an answer's end, a tool): the cache's age
61let zaman_akharin_kar = 0
62// set by classic.SessionStart on a resume: Claude Code's own word that the cache is gone
63let cache_sard_shod = false
64// a «پرسش» that came before the fork had anything to read (resume, a turn still running):
65// asked by itself when the next answer ends
66let porsesh_montazer = null
67// «بپرس»: a side question like /btw, answered by $.model.fork; never reaches the chat
68let javab_porsesh = null
69let matn_porsesh = ''
70// «پرسش» with Habib's saved prompts (26 Oct 05:41: «اون بپرس یه قسمت پرومپت هم داشته باشه که توش بتونم متن بزارم
71// و بشه یک دکمه … دکمه ادیت و ذخیره هم داشته باشه … اون سوالی که از مدل با کش گرم میپرسیم با این پرومپت پرسیده بشه»;
72// 05:44: «چند دکمه باشه و روی دکمه علامت ضربدر و ادیت باشه که زیاد بزرگ نشه و کلا یه علامت + برای اضافه کردن پرومپت»).
73// The list is the same in every chat (~/.claude/hads-prompt-ha.json); which one is on, per chat
74let prompt_ha = null
75const prompt_entekhab_har_chat = new Map()
76let prompt_virayesh = null
77
78export function khandan_hads(raw) {
79 try {
80 const m = String(raw || '').match(/\[[\s\S]*\]/)
81 const list = JSON.parse(m ? m[0] : raw)
82 if (!Array.isArray(list)) return []
83 return list
84 .filter((g) => g && typeof g.label === 'string' && typeof g.text === 'string' && g.text.trim())
85 .slice(0, 2)
86 .map((g) => ({ label: g.label.trim().slice(0, 40), text: g.text.trim() }))
87 } catch {
88 return []
89 }
90}
91
92const system_reminder_regex = /<system-reminder[\s\S]*?<\/system-reminder>/g
93export const az_agent_digar = (t) => /<cross-session-message|^\s*Another Claude session sent a message/.test(t)
94// what starts a turn but is not Habib's: a task's or another chat's message, or what Claude Code sends
95// by itself after a compact or a resume (205, 25 Oct: «Continue from where you left off.» was logged as
96// his request). Such a turn has no new word of his: the summary says so
97export const matn_bi_harfe_habib = 'حرفِ تازهای از حبیب نبود؛ ادامهٔ کارِ قبلی'
98export const harf_habib = (t) => !t || az_agent_digar(t) || /^\s*(<task-notification|Continue from where you left off|This session is being continued)/.test(t) ? matn_bi_harfe_habib : t
99
100// Labelled context: Habib's own words, Claude's answers, and other agents'
101// messages kept apart, so guesses follow Habib and still know the story.
102export function sakht_prompt(messages) {
103 const cut = (s, n) => String(s).slice(0, n)
104 const users = [], agents = []
105 for (const m of messages) {
106 if (m.role !== 'user' || !m.text) continue
107 if (az_agent_digar(m.text)) { agents.push(m.text); continue }
108 const own = m.text.replace(system_reminder_regex, '').trim()
109 if (own) users.push(own)
110 }
111 const answers = messages.filter((m) => m.role === 'assistant' && m.text).slice(-5)
112 return '## درخواستهای حبیب (قدیمی به جدید)\n' +
113 users.slice(-12).map((t, i) => (i + 1) + '. ' + cut(t, 1500)).join('\n') +
114 '\n\n## جوابهای کلود\n' + answers.map((m) => '- ' + cut(m.text, 1200)).join('\n') +
115 '\n\n## پیامِ ایجنتِ دیگر، نه حبیب\n' + (agents.slice(-3).map((t) => '- ' + cut(t, 1200)).join('\n') || '(هیچ)')
116}
117
118// .remember handoffs of this project: now, today, last seven days.
119async function handoff_ha($) {
120 const dir = '.remember/'
121 const read = async (f) => { try { return String(await $.fs.read(dir + f)).slice(0, 3000) } catch { return '' } }
122 let today = ''
123 try {
124 const names = (await $.fs.list(dir)).map((x) => x.name).filter((n) => /^today-.*\.md$/.test(n) && !n.includes('.done')).sort()
125 if (names.length) today = await read(names[names.length - 1])
126 } catch {}
127 const parts = [['الان', await read('now.md')], ['امروز', today], ['هفت روزِ اخیر', await read('recent.md')]]
128 .filter(([, t]) => t.trim())
129 return parts.length ? '## هندآفهای این پروژه\n' + parts.map(([k, t]) => '### ' + k + '\n' + t).join('\n') : ''
130}
131
132async function hads_ba_sonnet($) {
133 if (hads_dar_kar) return
134 hads_dar_kar = true
135 try {
136 const r = await $.model.complete({
137 model: model_do_jomle_va_hads, system: prompt_system_hads,
138 // handoffs first, cached: they change rarely between answers
139 prompt: [{ text: (await handoff_ha($)) || '(no handoff)', cache: true }, { text: sakht_prompt(await $.session.messages()) }],
140 maxTokens: 600, timeoutMs: 60000,
141 })
142 hads_akhar = r.isAnswered ? khandan_hads(r.text) : []
143 if (hads_akhar.length) await benevis_daftar($, 'hads', { rah: 'sonnet', hads_ha: hads_akhar.map((g) => g.text) })
144 if (r.isAnswered) try { await $.store.set('kharj_akhar_hads_sonnet', r.usage) } catch {}
145 if (r.isAnswered) await benevis_kharj($, 'hads', 'sonnet', r.usage)
146 hads_baraye_chat = await $.session.id()
147 $.ui.invalidate('ui.render')
148 } catch {
149 hads_akhar = []
150 } finally {
151 hads_dar_kar = false
152 }
153}
154
155// only the parts ticked on are asked for: fewer output tokens
156const prompt_fork_do_jomle = '"do_jomle": "<exactly two short sentences of plain Persian: what the agent in this chat is doing now and what it waits for, in حبیب\'s own words from the glossary below>"'
157const prompt_fork_hads = '"hads": [1 or 2 of {"label": "<= 3 words", "text": "<one line, ready to send as حبیب\'s next message, in his tone>"}]'
158// «نکردهها»: the fork has read the whole chat, every tool's output included. Its rules come from two
159// Sonnet audits of this chat (25 Oct 03:33): only Habib's own words; done means a tool proved it; his
160// later word replaces the earlier; a promise not kept counts; other items first than last time
161const prompt_fork_nakarde = () => '"nakarde": [all of them, 0 or more, each {"harf": "<حرفِ عینِ حبیب، حداکثر ۲۵ کلمه، بلندتر با … کوتاه>", "zaman": "<کی گفت، به وقتِ تهران: از مُهرِ ساعتِ پیامش مثلِ [01:03 +0330 -- habib] ⇒ ۲۵ اکت ۰۱:۰۳>", "chera": "<یک جملهٔ کوتاه: چرا هنوز انجام نشده، با مدرک از همین چت>", "sanad": <اختیاری: {"file": "<مسیرِ کاملِ فایلی که اگر انجام شده بود این متن در آن بود>", "matn": "<آن متن، کوتاه و عیناً>"} یا {"git": "<مسیرِ کاملِ پوشهٔ پروژه>", "matn": "<کلمهای که در پیامِ commit ِ آن کار میآمد>"}>}]'
162const ghaede_nakarde = (anjam, commit_ha = '', ferestade = []) => '\n\n«nakarde» (نکردهها): کلِ همین چت را از اول در حافظهٔ خودت مرور کن و همهٔ کارهایی را بیاور که حبیب خواست و هنوز انجام نشده، بیسقف. ' +
163 '۱) فقط حرفِ خودِ حبیب. پیامِ ایجنتِ دیگر، system-reminder، task-notification و «Continue from where you left off» حرفِ حبیب نیست؛ حرفی از حبیب که ایجنتی عینش را نقل کرده حساب است. ' +
164 '۲) «انجام شد» یعنی خروجیِ یک ابزار در همین چت ثابتش کرد (آزمونِ سبز، commit، فایلِ نوشتهشده). «انجام میدهم» یا «منتظرِ …» انجام نیست. ' +
165 '۳) حرفِ تازهترِ حبیب جای حرفِ قدیمیاش را میگیرد؛ چیزی که بعداً رد یا عوض کرد، نکرده نیست. ' +
166 '۴) قولی که به حبیب یا به ایجنتِ دیگر دادی و عمل نکردی («میپرسم»، «خبر میدهم»، «بعد از ریلود میزنم») هم نکرده است. ' +
167 '۵) مهمترین اول: آنچه حبیب رویش تأکید کرد، یا قدیمیتر و فراموششده. ' +
168 (anjam.length ? '۶) حبیب اینها را خودش «انجام شد» زد؛ هرگز دوباره نیاورشان، حتی با کلمههای دیگر: ' + anjam.map((x) => '«' + x + '»').join(' · ') + '. ' : '') +
169 // Habib, 25 Oct 19:08, through ۲۰۵: «بله، بسازد» — a «نکرده» whose reason is stale (done outside this
170 // chat, by a file or a commit) must not stay: the fork gives a check, the mod reads the file or the log
171 '۷) چیزی که بیرون از این چت انجام شده هم نکرده نیست: حالِ امروزِ پروژه را از commitهای زیر بخوان، و برای هر نکردهای که با یک فایل یا commit سنجیدنی است sanad بده؛ پیش از نمایش، فایل یا git log خوانده میشود و اگر آن متن آنجا بود، آن ردیف نمیآید. ' +
172 (commit_ha ? '\ncommitهای اخیرِ این پروژه (تازه به قدیم):\n' + commit_ha + '\n' : '') +
173 // Habib, 26 Oct 03:06: «اگه انجام نشدن به صورت نکرده بیان»: what the mod sent as «خودکار» is his ask too
174 // 26 Oct 07:44: «مگر اینکه حبیب بگوید نه انوقت نکرده حذف شود یا دیده شود که کار انجام شده»
175 (ferestade.length ? '۸) این «خودکار»ها را مودِ «حدس» به خواستِ حبیب به چت فرستاد و حکمِ خواستهٔ حبیب را دارند (استثنای قاعدهٔ ۱): هر کدام انجام نشده، در nakarde بیاور با harf = عینِ همین متن: ' + ferestade.map((x) => '«' + x + '»').join(' · ') + '. بهانهٔ ایجنت («الان نمیشود»، «منتظرِ ریلود/پنجره/…») آن را از نکرده بیرون نمیبرد؛ فقط دو چیز بیرونش میبرد: حبیب گفته باشد نه، یا انجام شدنش با خروجیِ یک ابزار دیده شود. ' : '') +
176 'نامطمئن ⇒ نیاور. هیچ نبود ⇒ []. فارسیِ ساده، با واژههای حبیب.'
177// Habib, 25 Oct 18:08: «منظورم کل ابزارهایی هست که توی کلود کد برای ایجنت ها موجوده. و این ابزارها
178// باید سرچ به وب بکنی که لیست کاملش رو در بیاری و توی پرومپتت بیاری که ایجنت چک بکنه و بررسی بکنه و
179// پیشنهاداشو بده». The built-in tools as code.claude.com/docs/en/tools-reference lists them (read 25 Oct
180// 2026), then the MCP ones and this Mac's own ways (Habib's rules: computer use only by ۲۰۳, each agent its own Chrome)
181export const abzar_ha_ye_claude_code = [
182 ['Agent', 'سابایجنت با کانتکستِ جدا برای یک کار (پیشفرض Sonnet؛ تعداد و مدل با اجازهٔ حبیب)'],
183 ['Bash', 'هر فرمانِ شل: آزمون، اسکریپت، git، curl، شبیهسازِ iOS (xcrun simctl)، launchd و crontab، عکسِ پنجره بیجلوآوردن (screencapture -x -o -l)'],
184 ['Read', 'خواندنِ فایل، عکس و PDF'],
185 ['Write', 'ساختن یا بازنویسیِ فایل'],
186 ['Edit', 'ویرایشِ دقیقِ فایل'],
187 ['NotebookEdit', 'ویرایشِ سلولِ Jupyter'],
188 ['Glob', 'پیدا کردنِ فایل با الگو'],
189 ['Grep', 'گشتن در متنِ فایلها'],
190 ['LSP', 'تعریف، ارجاع و خطای تایپِ کد از language server'],
191 ['WebSearch', 'سرچِ وب'],
192 ['WebFetch', 'خواندنِ یک صفحهٔ وب'],
193 ['Monitor', 'فرمانی در پسزمینه که هر خطِ خروجیاش (لاگ، تغییرِ فایل، وضعیت، پیامِ WebSocket) به ایجنت میرسد'],
194 ['CronCreate، CronList، CronDelete', 'کارِ زماندار یا تکراری در همین چت'],
195 ['ScheduleWakeup', 'نوبتِ بعدیِ /loop را خودِ ایجنت تعیین میکند'],
196 ['RemoteTrigger', 'کارِ زماندارِ ابری روی claude.ai (/schedule)'],
197 ['PushNotification', 'خبر به دسکتاپ و گوشیِ حبیب وقتی کارِ طولانی تمام شد'],
198 ['SendUserFile', 'فرستادنِ فایل، گزارش یا عکس به دستگاهِ حبیب'],
199 ['SendMessage، ListAgents', 'پیام به ایجنتِ دیگر یا چتِ دیگرِ همین مک (۲۰۳ برای کامپیوتر یوز، منشی برای کروم)'],
200 ['TaskCreate، TaskGet، TaskList، TaskUpdate، TaskStop', 'فهرستِ کارها و توقفِ کارِ پسزمینه'],
201 ['Workflow', 'چند سابایجنت با هم در پسزمینه (فقط با خواستِ صریحِ حبیب)'],
202 ['Skill', 'اجرای اسکیل، مثلاً chrome-cdp، nazer، remember'],
203 ['ToolSearch، WaitForMcpServers', 'بار کردنِ ابزارهای پنهان و صبر برای سرورِ MCP'],
204 ['ListMcpResourcesTool، ReadMcpResourceTool', 'خواندنِ منابعِ سرورهای MCP'],
205 ['EnterWorktree، ExitWorktree', 'کار روی کپیِ جدای git'],
206 ['EnterPlanMode، ExitPlanMode', 'نقشه پیش از کد'],
207 ['Artifact', 'صفحهٔ HTML روی claude.ai برای دیدن یا فرستادن'],
208 ['AskUserQuestion', 'پنجرهٔ سؤال؛ فقط برای تصمیمی که واقعاً مالِ حبیب است'],
209 ['ReportFindings، SendFeedback، ShareOnboardingGuide، SubagentHandback، TodoWrite، PowerShell، EndConversation', 'ابزارهای خاص؛ کم لازم میشوند'],
210 ['کامپیوتر یوز (MCP)', 'دیدنِ صفحهٔ مک، کلیک و تایپ در اپها و دیدنِ نتیجه با عکس؛ قانونِ حبیب: فقط ۲۰۳ ⇒ با SendMessage به ۲۰۳'],
211 ['کرومِ CDP ِ خودِ ایجنت (اسکیلِ chrome-cdp)', 'سایت در تبِ پسزمینه، کلیک، خواندنِ متنِ صفحه، عکس با Page.captureScreenshot؛ پنجره هرگز جلو نیاید؛ کرومِ بیپنجره فقط برای آزمون'],
212 ['Claude in Chrome (/chrome) و مرورگرِ داخلی', 'مرورگر با لاگینِ خودِ حبیب: فرم، کنسول، ضبطِ GIF'],
213 ['hook و mod', 'کاری که خودکار پیش یا پس از هر ابزار یا پیام اجرا شود (settings.json)'],
214 ['سرچ در چتهای قبلی', 'python3 ~/1-MY-WORK/205th-day-mother-of-all-agent/abzar/hafeze_search.py --hame "<سؤال>" 5 ⇒ حبیب در موردِ مشابه چه گفت'],
215]
216const prompt_fork_khodkar = '"khodkar": [all of them, 0 or more, each {"kar": "<کاری که الان در این چت جاری است و ایجنت آن را از حبیب میپرسد یا به او میسپارد، کوتاه>", "abzar": "<کدام ابزار از فهرست، دقیق: اسمِ ابزار و فرمان یا مسیر>", "chek": "<ایجنت نتیجه را خودش چطور میبیند و میسنجد>", "chera": "<یک جملهٔ کوتاه: چرا لازم نیست از حبیب بپرسد>"}]'
217const ghaede_khodkar = (ferestade = []) => '\n\n«khodkar» (خودکار): این فهرستِ کاملِ ابزارهای ایجنت در Claude Code است (code.claude.com/docs/en/tools-reference، خواندهشده ۲۵ اکت ۲۰۲۶):\n' +
218 abzar_ha_ye_claude_code.map(([n, t]) => '- ' + n + ': ' + t).join('\n') + '\n' +
219 'یکییکی چک و بررسی کن: در کارهایی که الان در همین چت جاری است، کدام ابزار کاری را که ایجنت از حبیب میپرسد یا به او میسپارد («چک کن به من بگو»، «امتحان کن»، «ببین درست است؟»، «ریلود کن و عکس بفرست») خودش انجام میدهد و نتیجهاش را خودش چک میکند. پیشنهادهایت را بده، بیسقف. ' +
220 'ابزاری که در این چت استفاده نشده و کار را عقب انداخته، اول بیاید. پیش از هر تصمیم، سرچ در چتهای قبلی برای حرفِ حبیب در موردِ مشابه و تصمیم طبقِ همان حرف؛ مثلاً برای چکِ یک اپ، اول پیدا کن اپ مالِ کدام پروژه است و حبیب قبلاً دربارهاش چه گفته. ' +
221 'نیاور: رمز، کدِ پیامکی، پول، کارِ دست یا تنِ حبیب، و چیزی که قانونِ حبیب صریحاً با او گذاشته. نامطمئن ⇒ نیاور. هیچ نبود ⇒ []. فارسیِ ساده، با واژههای حبیب.' +
222 // Habib, 26 Oct 07:44: «اتفاقا این رو برای همین میخوام که مدل منتظر نمونه و یه راه حلی پیدا کنه براش نه اینکه با منتظر بودن
223 // کار رو به تعویق بندازه — به مدل بگو بهونه های مدل رو سعی کنه براش یه راهکاری پیذا کنه از کارهای قبلی و تجارب قبلی و با
224 // راهکار جدید تاکید کنه به انجام کار مگه با دستورر مخالف حبیب»
225 (ferestade.length ? '\nاینها را قبلاً به چت فرستادی (کار ⇐ راه): ' + ferestade.map((x) => '«' + x + '»').join(' · ') + '. همان کار با همان راه را دوباره نساز، حتی با کلمههای دیگر. ' : '\n') +
226 'بهانه، راهکار میخواهد نه صبر: اگر ایجنت کاری را نکرد و بهانه آورد («الان نمیشود»، «منتظرِ ریلود / جلو آمدنِ پنجره / حبیب / تمام شدنِ کارِ پسزمینه»)، منتظر نمان؛ از کارها و تجربههای قبلیِ همین چت و چتهای قبلی (سرچ در چتهای قبلی) برای همان بهانه راهکار پیدا کن، مثلاً ایجنتِ دیگری که آن ابزار را دارد (کامپیوتر یوز ⇐ ۲۰۳)، و همان کار را با راهِ تازه بیاور؛ در chera بنویس بهانه چه بود و چرا راهِ تازه از پسش برمیآید. فقط اگر حبیب صریحاً خلافش را گفته، نیاور.'
227// «کرونجاب» (Habib, 25 Oct 16:52: «کرون جا درست کنه … با توجه به اون کارایی که من مشخص میکنم اون
228// کاراییش که میخوره»): of the work Habib named, what fits a job that runs by itself on a schedule
229const prompt_fork_cron = '"cron": [all of them, 0 or more, each {"kar": "<کاری که حبیب در این چت مشخص کرد و به کرونجاب میخورد>", "zaman": "<کی اجرا شود، به فارسی و با عبارتِ cron، مثلاً هر روز ۰۸:۰۰ (0 8 * * *)>", "rah": "<با چه ساخته شود: launchd، crontab، یا CronCreate/schedule ِ Claude Code، و فرمانش>"}]'
230const ghaede_cron = '\n\n«cron» (کرونجاب): از میانِ کارهایی که حبیب در همین چت مشخص کرد، هر کدام که تکراری است یا باید سرِ وقت یا هر چند وقت خودش اجرا یا چک شود (چکِ نسخه، نگهبان، گزارشِ روزانه، پاک کردنِ دفتر، …)، بیسقف. کرونجابی که در همین چت ساخته شده و کار میکند نیاور. نامطمئن ⇒ نیاور. هیچ نبود ⇒ [].'
231const prompt_fork_bakhsh_ha = (p, anjam) => [p.do_jomle && prompt_fork_do_jomle, p.hads && prompt_fork_hads, p.nakarde && prompt_fork_nakarde(), p.khodkar && prompt_fork_khodkar, p.cron && prompt_fork_cron].filter(Boolean)
232const ghaede_bakhsh_ha = (p, anjam) => (p.nakarde ? ghaede_nakarde(anjam, p.commit_ha, p.khodkar_ferestade || []) : '') + (p.khodkar ? ghaede_khodkar(p.khodkar_rah || []) : '') + (p.cron ? ghaede_cron : '')
233const prompt_fork = (p, anjam = []) => '(این پیام از مودِ «حدس» است، نه از حبیب؛ کاری نکن و ابزاری صدا نزن.) Reply ONLY with JSON: {' +
234 prompt_fork_bakhsh_ha(p, anjam).join(', ') + '}' + ghaede_bakhsh_ha(p, anjam)
235
236// «نکردهها» from the fork's reply: all of them (Habib, 25 Oct 04:53: «محدودیت نداشته باشن تعدادشون»),
237// each with Habib's words, when, and why
238export function khandan_nakarde(list) {
239 return (Array.isArray(list) ? list : [])
240 .filter((x) => x && typeof x.harf === 'string' && x.harf.trim() && typeof x.chera === 'string')
241 .map((x) => ({ harf: x.harf.trim(), zaman: typeof x.zaman === 'string' ? x.zaman.trim() : '', chera: x.chera.trim(), ...(x.khodkar === true ? { khodkar: true } : {}), ...(khandan_sanad(x.sanad) ? { sanad: khandan_sanad(x.sanad) } : {}) }))
242}
243// a check the mod can run itself: an absolute file that would hold the text, or a project whose commit
244// messages would; anything else is no check
245export const had_kamtarin_harf_sanad = 8
246export function khandan_sanad(x) {
247 if (!x || typeof x !== 'object' || typeof x.matn !== 'string' || !x.matn.trim()) return null
248 const matn = x.matn.trim().slice(0, 200)
249 // a word that short is in too many files to prove anything ("dayere", "on")
250 if ([...matn].length < had_kamtarin_harf_sanad) return null
251 if (typeof x.file === 'string' && x.file.startsWith('/')) return { file: x.file, matn }
252 if (typeof x.git === 'string' && x.git.startsWith('/')) return { git: x.git, matn }
253 return null
254}
255async function sanad_dare($, sanad) {
256 if (!sanad) return false
257 try {
258 if (sanad.file) return (await $.process.run(['grep', '-F', '-q', '--', sanad.matn, sanad.file], { timeoutMs: 5000 })).exitCode === 0
259 const r = await $.process.run(['git', '-C', sanad.git, 'log', '-200', '--format=%s%n%b'], { timeoutMs: 5000 })
260 return r.exitCode === 0 && String(r.stdout || '').includes(sanad.matn)
261 } catch {
262 return false
263 }
264}
265// the project's last commits, newest first, for the fork to see what is done today
266async function commit_ha_ye_akhir($) {
267 try {
268 const r = await $.process.run(['git', '-C', await $.session.root(), 'log', '-20', '--date=format:%m-%d %H:%M', '--format=%h %ad %s'], { timeoutMs: 5000 })
269 return r.exitCode === 0 ? String(r.stdout || '').trim().slice(0, 4000) : ''
270 } catch {
271 return ''
272 }
273}
274// «خودکار» from the fork's reply: all of them, each with the work, the tool, how the agent checks it, and why not to ask
275export function khandan_khodkar(list) {
276 return (Array.isArray(list) ? list : [])
277 .filter((x) => x && typeof x.kar === 'string' && x.kar.trim() && typeof x.abzar === 'string' && x.abzar.trim())
278 .map((x) => ({ kar: x.kar.trim(), abzar: x.abzar.trim(), chek: typeof x.chek === 'string' ? x.chek.trim() : '', chera: typeof x.chera === 'string' ? x.chera.trim() : '' }))
279}
280export function khandan_cron(list) {
281 return (Array.isArray(list) ? list : [])
282 .filter((x) => x && typeof x.kar === 'string' && x.kar.trim() && typeof x.zaman === 'string' && x.zaman.trim())
283 .map((x) => ({ kar: x.kar.trim(), zaman: x.zaman.trim(), rah: typeof x.rah === 'string' ? x.rah.trim() : '' }))
284}
285// what Habib pressed «انجام شد» on, per chat (~/.claude/hads-log/<id>.nakarde_anjam_shod.jsonl): never
286// shown again — the fork is told, and anything that says the same is dropped here too
287const masir_nakarde_anjam = async ($) => (await $.env.get('HOME')) + '/.claude/hads-log/' + (await $.session.id()) + '.nakarde_anjam_shod.jsonl'
288const nakarde_anjam_har_chat = new Map()
289async function khandan_nakarde_anjam($) {
290 const id = await $.session.id()
291 if (!nakarde_anjam_har_chat.has(id)) {
292 let list = []
293 try { list = String(await $.fs.read(await masir_nakarde_anjam($))).split('\n').filter(Boolean).map((l) => JSON.parse(l).harf) } catch {}
294 nakarde_anjam_har_chat.set(id, list)
295 }
296 return nakarde_anjam_har_chat.get(id)
297}
298export const hanuz_nakarde = (list, anjam) => list.filter((x) => !anjam.some((a) => a === x.harf || hamun_matn(a, x.harf)))
299async function anjam_shod_zadan($, harf) {
300 const anjam = await khandan_nakarde_anjam($)
301 if (!anjam.includes(harf)) anjam.push(harf)
302 nakarde_akhar = hanuz_nakarde(nakarde_akhar, anjam)
303 $.ui.invalidate('ui.render')
304 try {
305 const line = JSON.stringify({ t: await $.clock.now(), harf }) + '\n'
306 await $.process.run(['sh', '-c', 'mkdir -p "$(dirname "$1")" && cat >> "$1"', 'sh', await masir_nakarde_anjam($)], { stdin: line, timeoutMs: 5000 })
307 } catch {}
308 await benevis_daftar($, 'nakarde_anjam_shod', { harf })
309}
310
311// {do_jomle, hads, nakarde, khodkar} from the fork's reply; null when it has none
312export function khandan_javab_fork(raw) {
313 try {
314 const m = String(raw || '').match(/\{[\s\S]*\}/)
315 const o = JSON.parse(m ? m[0] : raw)
316 const n = typeof o?.do_jomle === 'string' ? kutah_be_do_jomle(o.do_jomle) : ''
317 const g = Array.isArray(o?.hads) ? khandan_hads(JSON.stringify(o.hads)) : []
318 const nk = khandan_nakarde(o?.nakarde)
319 const kh = khandan_khodkar(o?.khodkar)
320 const cr = khandan_cron(o?.cron)
321 return n || g.length || nk.length || kh.length || cr.length || Array.isArray(o?.nakarde) || Array.isArray(o?.khodkar) || Array.isArray(o?.cron) ? { do_jomle: n, hads: g, nakarde: nk, khodkar: kh, cron: cr } : null
322 } catch {
323 return null
324 }
325}
326
327// $ per million tokens written to the cache (5 min, 1 h), for the cold «بپرس» warning
328const gheymat_cache_neveshtan_usd = { 'opus-5-5': [5, 8], 'sonnet-5-5': [2.5, 4], 'haiku-5-5': [0.125, 0.2] }
329export function kharj_cache_sard(model, tokens) {
330 const k = Object.keys(gheymat_cache_neveshtan_usd).find((x) => String(model).includes(x)) || 'opus-5-5'
331 return gheymat_cache_neveshtan_usd[k].map((p) => (tokens * p) / 1e6)
332}
333
334// «قبلیها»: every «دو جمله» with its time, this chat only (~/.claude/hads-log/<id>.do_jomle_ghablia.jsonl,
335// the last had_ghablia_tedad); newest first on screen
336export const had_ghablia_tedad = 50
337const masir_ghablia = async ($) => (await $.env.get('HOME')) + '/.claude/hads-log/' + (await $.session.id()) + '.do_jomle_ghablia.jsonl'
338async function khandan_ghablia($) {
339 try {
340 return String(await $.fs.read(await masir_ghablia($))).split('\n').filter(Boolean).map((l) => JSON.parse(l)).reverse()
341 } catch {
342 return []
343 }
344}
345// «دفترِ حدس» (with چتِ ۲۰۵, 24 Oct): every «دو جمله», guess, «پرسش» and what Habib did with a
346// guess, one JSON line each in ~/.claude/hads-log/<id>.events.jsonl — no cap, appended, never
347// read back here (a 4 MB read limit would cut a long chat). Their memory index reads it; only
348// «پرسش» and a guess Habib sent go to search, never «دو جمله» or an unsent guess.
349async function benevis_daftar($, kind, data) {
350 try {
351 const t = await $.clock.now(), sid = await $.session.id()
352 const d = new Date(t + 3.5 * 3600e3)
353 const iso = d.toISOString().replace('Z', '+03:30')
354 const f = (await $.env.get('HOME')) + '/.claude/hads-log/' + sid + '.events.jsonl'
355 const line = JSON.stringify({ t, iso, sid, kind, ...data }) + '\n'
356 await $.process.run(['sh', '-c', 'mkdir -p "$(dirname "$1")" && cat >> "$1"', 'sh', f], { stdin: line, timeoutMs: 5000 })
357 } catch {}
358}
359// a «دو جمله» that says what the last one said is not logged again (205: so the log stays readable)
360export function hamun_matn(a, b) {
361 const w = (x) => new Set(String(x || '').split(/\s+/).filter((y) => y.length > 1))
362 const A = w(a), B = w(b)
363 if (!A.size || !B.size) return false
364 let n = 0; for (const x of A) if (B.has(x)) n++
365 return n / Math.max(A.size, B.size) >= 0.8
366}
367// per chat id: one process may serve more than one chat (/clear makes a new id)
368const do_jomle_akhar_dar_daftar = new Map()
369// guesses pressed into the prompt box, to know at Enter which one Habib sent
370let hads_zade_shode = []
371
372// every «دو جمله» of the turn now running, per chat: the end of the answer sums them up
373const do_jomle_ha_ye_nobat = new Map()
374async function benevis_do_jomle($, text, kind = 'do_jomle') {
375 const id = await $.session.id()
376 if (kind === 'do_jomle') do_jomle_ha_ye_nobat.set(id, [...(do_jomle_ha_ye_nobat.get(id) || []), text].slice(-40))
377 do_jomle_akhar = { text, for: id }; $.ui.invalidate('ui.render')
378 if (kind !== 'jambandi_nobat' && !hamun_matn(text, do_jomle_akhar_dar_daftar.get(id))) { do_jomle_akhar_dar_daftar.set(id, text); await benevis_daftar($, 'do_jomle', { text }) }
379 try {
380 const old = (await khandan_ghablia($)).reverse().map((x) => JSON.stringify(x))
381 await $.fs.write(await masir_ghablia($), old.slice(-(had_ghablia_tedad - 1)).concat(JSON.stringify({ t: await $.clock.now(), text })).join('\n') + '\n')
382 } catch {}
383}
384// Tehran time (+03:30, no DST), as Habib writes dates: «۲۴ اکت ۱۸:۵۷»
385const mah_miladi = ['ژان', 'فور', 'مار', 'آور', 'مه', 'ژوئن', 'ژوئیه', 'اوت', 'سپت', 'اکت', 'نوا', 'دسا']
386const adad_farsi = (x) => String(x).replace(/\d/g, (d) => '۰۱۲۳۴۵۶۷۸۹'[d])
387export function zaman_miladi(t) {
388 const d = new Date(t + 3.5 * 3600e3)
389 const two = (x) => String(x).padStart(2, '0')
390 return adad_farsi(d.getUTCDate()) + ' ' + mah_miladi[d.getUTCMonth()] + ' ' + adad_farsi(two(d.getUTCHours()) + ':' + two(d.getUTCMinutes()))
391}
392
393// Whatever the model says, at most two sentences are shown. A sentence ends at . ! ? ؟
394// followed by a space: the dot inside register.js or v2.10 ends nothing. Never past had_do_jomle_harf.
395// Shamsi (Jalali) date and Tehran time, Persian digits: «۳ آبان ۱۴۰۵ ۲۰:۰۹». Habib: «اگر که
396// ساعت تاریخ شمسی باشه چه بهتر». Tehran is +03:30 all year (no DST since 1401).
397const mah_shamsi = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
398export function tarikh_shamsi(gy, gm, gd) {
399 const g = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
400 const gy2 = gm > 2 ? gy + 1 : gy
401 let days = 355666 + 365 * gy + Math.floor((gy2 + 3) / 4) - Math.floor((gy2 + 99) / 100) + Math.floor((gy2 + 399) / 400) + gd + g[gm - 1]
402 let jy = -1595 + 33 * Math.floor(days / 12053); days %= 12053
403 jy += 4 * Math.floor(days / 1461); days %= 1461
404 if (days > 365) { jy += Math.floor((days - 1) / 365); days = (days - 1) % 365 }
405 const jm = days < 186 ? 1 + Math.floor(days / 31) : 7 + Math.floor((days - 186) / 30)
406 const jd = 1 + (days < 186 ? days % 31 : (days - 186) % 30)
407 return [jy, jm, jd]
408}
409export function zaman_shamsi(t) {
410 if (!t) return ''
411 const d = new Date(t + 3.5 * 3600e3)
412 const [jy, jm, jd] = tarikh_shamsi(d.getUTCFullYear(), d.getUTCMonth() + 1, d.getUTCDate())
413 const hh = String(d.getUTCHours()).padStart(2, '0'), mm = String(d.getUTCMinutes()).padStart(2, '0')
414 return adad_farsi(jd + ' ' + mah_shamsi[jm - 1] + ' ' + jy + ' ' + hh + ':' + mm)
415}
416
417export const had_do_jomle_harf = 300
418export function kutah_be_do_jomle(raw) {
419 const t = String(raw || '').replace(/\s+/g, ' ').trim().split(/(?<=[.!?؟]) /).slice(0, 2).join(' ')
420 return t.length > had_do_jomle_harf ? t.slice(0, had_do_jomle_harf - 1) + '…' : t
421}
422
423async function vazhe_name($) {
424 for (const f of ['Doc/tarif/00_vazhe_name.md', 'doc/tarif/00_vazhe_name.md']) {
425 try { return '## واژهنامهٔ حبیب\n' + String(await $.fs.read((await $.session.root()) + '/' + f)).slice(0, 6000) } catch {}
426 }
427 return '(no glossary: plain Persian)'
428}
429
430// Sonnet on the last three messages only: cheap enough to run every do_jomle_fasele_ms during work
431// every model call of this chat, one JSON line each (~/.claude/hads-log/<id>.usage.jsonl,
432// the last 500): the real cost per part, read by `hads-kharj`
433async function benevis_kharj($, bakhsh, rah, usage) {
434 try {
435 const f = (await $.env.get('HOME')) + '/.claude/hads-log/' + (await $.session.id()) + '.usage.jsonl'
436 // appended, never read and rewritten: two calls ending together would each drop the other's line
437 const line = JSON.stringify({ t: await $.clock.now(), bakhsh, rah, usage }) + '\n'
438 await $.process.run(['sh', '-c', 'mkdir -p "$(dirname "$1")" && cat >> "$1"', 'sh', f], { stdin: line, timeoutMs: 5000 })
439 } catch {}
440}
441
442// «دو جمله» reads the last three messages and, always, Habib's last two: deep in a long answer
443// the last three are all the agent's, and his request was missing (24 Oct)
444export function payam_ha_ye_akhar(msgs) {
445 const last = msgs.slice(-3)
446 const his = msgs.filter((m) => m.role === 'user' && m.text && !az_agent_digar(m.text) && m.text.replace(system_reminder_regex, '').trim()).slice(-2)
447 return msgs.filter((m) => last.includes(m) || his.includes(m))
448}
449
450async function begu_do_jomle($, tool, force, viaFork = false) {
451 // an answer's end while a mid-work line is on its way: told right after it, never dropped
452 if (do_jomle_dar_kar) { if (force) do_jomle_dobare = true; return }
453 const t = await $.clock.now()
454 if (!force && t - zaman_do_jomle_akhar < do_jomle_fasele_ms) return
455 do_jomle_dar_kar = true; zaman_do_jomle_akhar = t
456 try {
457 let done = false
458 if (viaFork) {
459 const r = await $.model.fork({ prompt: prompt_fork({ do_jomle: true }) + '\n\n' + (await vazhe_name($)) })
460 const got = r.isAnswered ? khandan_javab_fork(r.text) : null
461 if (r.isAnswered) await benevis_kharj($, 'do_jomle', 'fork', r.usage)
462 // no early return: a queued answer-end line (nowAgain) must still be told below
463 if (got?.do_jomle && zaman_akhar_javab <= t) { await benevis_do_jomle($, got.do_jomle, force ? 'akhar_javab' : 'do_jomle'); done = true }
464 }
465 if (!done) {
466 const r = await $.model.complete({
467 model: model_do_jomle_va_hads, system: prompt_system_do_jomle,
468 prompt: [{ text: await vazhe_name($), cache: true },
469 { text: sakht_prompt(payam_ha_ye_akhar(await $.session.messages())) + (tool ? '\n\n## ابزاری که همین الان تمام شد\n' + tool : '') }],
470 maxTokens: 200, timeoutMs: 30000,
471 })
472 // an answer's end started after this call: its line stands
473 if (r.isAnswered && zaman_akhar_javab <= t) {
474 // only lines written during the work belong to the turn's summary, not the answer's last one
475 await benevis_do_jomle($, kutah_be_do_jomle(r.text), force ? 'akhar_javab' : 'do_jomle')
476 try { await $.store.set('kharj_akhar_do_jomle', r.usage) } catch {}
477 }
478 if (r.isAnswered) await benevis_kharj($, 'do_jomle', 'sonnet', r.usage)
479 }
480 } catch {} finally {
481 do_jomle_dar_kar = false
482 }
483 if (do_jomle_dobare) { do_jomle_dobare = false; await begu_do_jomle($, '', true) }
484}
485
486async function chat_kuchak($) {
487 try { return ((await $.session.usage()).context.tokens || 0) <= had_fork_context_token } catch { return false }
488}
489
490// The end of an answer. «دو جمله»: Sonnet on the last messages, every answer (cheap).
491// The guesses: one fork over the whole chat (its own model, the cache this answer just
492// warmed) every hads_har_chand_javab answers; a chat over had_fork_context_token, or no usable reply, asks Sonnet.
493async function akhar_javab($, p = { hads: true, do_jomle: true }, force = false) {
494 // the scope may have been changed elsewhere (a terminal, the file): the label is read again each answer
495 khandan_dayere($).catch(() => {})
496 if (!p.hads && !p.do_jomle && !p.nakarde && !p.khodkar && !p.cron) return
497 if (akhar_javab_dar_kar) { akhar_javab_dobare = true; return }
498 akhar_javab_dar_kar = true; zaman_akhar_javab = await $.clock.now()
499 try {
500 const id = await $.session.id()
501 const seen = tedad_javab_har_chat.get(id) || 0
502 tedad_javab_har_chat.set(id, seen + 1)
503 // also due when this chat shows none (first answer, /clear, an empty or broken reply)
504 // «نکردهها» and «خودکار» come with the guesses, on the same answers
505 const due = (p.hads || p.nakarde || p.khodkar || p.cron) && (force || seen % hads_har_chand_javab === 0 || hads_baraye_chat !== id || (p.hads && !hads_akhar.length))
506 const pj = p.do_jomle ? await amade_jambandi($) : null
507 const jambandi_shod = due ? await hads_ba_fork($, p, pj) : await jambandi($, pj)
508 if (p.do_jomle && !jambandi_shod) await begu_do_jomle($, '', true)
509 await ferestadan_khodkar($)
510 } finally {
511 akhar_javab_dar_kar = false
512 }
513 // turned off meanwhile: the queued run is dropped, never run on the old flags
514 if (akhar_javab_dobare) { akhar_javab_dobare = false; const q = await khandan_file_chat($); if (q) await akhar_javab($, q) }
515}
516
517// The end of an exchange (Habib, 24 Oct 22:42: «دو جمله … عکس لحظه ای میگیره … جمع بندی نداره …
518// بعد از رفت و برگشت کل اینا رو بده به … یه ایجنت با کشه گرم که … جمعبندی کنه»): one fork of the
519// chat itself (warm cache: the answer was just sent) gets every «دو جمله» of this turn and says
520// what was done and what was not. Shown in place of «دو جمله» and logged as jambandi_nobat.
521// No tool and no line, or no usable reply: the plain Sonnet «دو جمله», as before. Any size of
522// chat is summed up by its own fork (24 Oct 23:37): it is injected into later chats' memory.
523// a turn is summed up when the agent did work in it: a tool ran, or «دو جمله» was written.
524// (v7 asked for 2 lines; «دو جمله» comes at most once a minute, so most turns had 0-1 and the
525// real chat ۲۰۳ never got one — 24 Oct 22:58)
526export const had_kamtarin_do_jomle_jambandi = 1
527// tools run in the turn now running, per chat
528const abzar_ha_ye_nobat = new Map()
529// Habib's own message that started the turn, per chat
530const khaste_habib_nobat = new Map()
531// the turn's summary and, when due, every other part (guesses, «نکردهها», «خودکار», «کرونجاب») in
532// ONE fork (Habib, 25 Oct 16:52: «همشون توی همین با یه مدل با یه دفعه انجام بشن نه هر کدوم یکی یه
533// دفعه … اینطور نشه پنجتا بار از اون کش گرم استفاده بشه»)
534const prompt_jambandi_nobat = (lines, tools, ezafe = null, anjam = []) => '(این پیام از مودِ «حدس» است، نه از حبیب؛ کاری نکن و ابزاری صدا نزن.) ' +
535 'جمعبندیِ همین نوبت را بده؛ این جمعبندی به حافظهٔ چتهای بعدی تزریق میشود، پس باید بیاشتباه باشد. ' +
536 'منبع: کلِ همین نوبت در حافظهٔ خودت، از آخرین پیامِ حبیب تا حالا: پیامِ حبیب، هر فراخوانیِ ابزار و خروجیاش، و جوابت. ' +
537 (lines.length ? 'این «دو جمله»ها وسطِ کار نوشته شدهاند؛ فقط راهنمااند، منبع نیستند:\n' + lines.map((l, i) => (i + 1) + ') ' + l).join('\n') + '\n' : '') +
538 (tools.length ? 'فهرستِ ابزارهای این نوبت (خروجیشان را از حافظهٔ خودت بخوان):\n' + tools.map((l) => '- ' + l).join('\n') + '\n' : '') +
539 '\nقاعدهها: ' +
540 '۱) در anjam_shod فقط چیزی که خروجیِ یک ابزار در همین نوبت ثابتش کرد، و ته هر کدام مدرکش بعد از «⇐» (مثلاً «نگهبان روشن است ⇐ pgrep: 36459»). ' +
541 '۲) پیشنهاد، نیت، کاری که فقط گفته شد، یا چیزی که از نوبتهای قبل میدانستی ⇒ anjam_shod نیست. ' +
542 '۳) نامطمئن یا سنجیدهنشده ⇒ anjam_nashod با «چک نشد». ' +
543 '۴) عدد، ساعت، اسمِ فایل و commit فقط اگر عیناً در خروجیِ یک ابزارِ همین نوبت آمده. ' +
544 '۵) هر خواستهٔ حبیب در همین نوبت که انجام نشد ⇒ anjam_nashod. ' +
545 '۶) مدرک فقط همان چیزی را ثابت میکند که خودش نشان میدهد: ویرایشِ کد ⇒ «کد اضافه شد»، نه «کار میکند»؛ رفتار فقط با آزمونی که همان رفتار را دید ⇒ وگرنه anjam_nashod «رفتارش آزموده نشد». فارسیِ ساده، با واژههای حبیب. ' +
546 'Reply ONLY with JSON: {"text": "<at most two short sentences: the whole turn>", "anjam_shod": ["<short> ⇐ <evidence>", …], "anjam_nashod": ["<short>", …]' +
547 (ezafe ? ', ' + prompt_fork_bakhsh_ha(ezafe, anjam).join(', ') + '}' + ghaede_bakhsh_ha(ezafe, anjam) : '}')
548export function khandan_jambandi(raw) {
549 try {
550 const m = String(raw || '').match(/\{[\s\S]*\}/)
551 const o = JSON.parse(m ? m[0] : raw)
552 const list = (x) => (Array.isArray(x) ? x : []).filter((y) => typeof y === 'string' && y.trim()).map((y) => y.trim()).slice(0, 8)
553 const text = typeof o?.text === 'string' ? kutah_be_do_jomle(o.text) : ''
554 return text ? { text, anjam_shod: list(o.anjam_shod), anjam_nashod: list(o.anjam_nashod) } : null
555 } catch {
556 return null
557 }
558}
559// this turn's lines and tools, taken (and cleared) once; null when the turn has nothing to sum up
560async function amade_jambandi($) {
561 const id = await $.session.id()
562 const lines = do_jomle_ha_ye_nobat.get(id) || []
563 const tools = abzar_ha_ye_nobat.get(id) || []
564 const asked = khaste_habib_nobat.get(id) || ''
565 do_jomle_ha_ye_nobat.set(id, []); abzar_ha_ye_nobat.set(id, [])
566 if (lines.length < had_kamtarin_do_jomle_jambandi && !tools.length) return null
567 return { lines, tools, asked }
568}
569// the summary alone, when the other parts are not due this answer
570async function jambandi($, pj) {
571 if (!pj) return false
572 try {
573 // always the chat itself with its warm cache, whatever its size: only it has read every tool's
574 // output (Habib, 24 Oct 23:37: «با مدل اصلی که با کش گرم کار میکنه»; Sonnet on the last few
575 // messages got turns wrong). No usable reply: no summary, the plain «دو جمله».
576 const r = await $.model.fork({ prompt: prompt_jambandi_nobat(pj.lines, pj.tools) })
577 if (r.isAnswered) await benevis_kharj($, 'jambandi_nobat', 'fork', r.usage)
578 return sabt_jambandi($, pj, r.isAnswered ? r.text : '')
579 } catch {
580 return false
581 }
582}
583async function sabt_jambandi($, pj, raw) {
584 const { lines, tools, asked } = pj
585 let tokens = 0
586 try { tokens = (await $.session.usage()).context.tokens || 0 } catch {}
587 try {
588 const got = khandan_jambandi(raw)
589 if (!got) return false
590 await benevis_do_jomle($, got.text, 'jambandi_nobat')
591 do_jomle_akhar.summary = got
592 // what Habib asked in this turn, word for word from his own message (not the model's retelling)
593 await benevis_daftar($, 'jambandi_nobat', { khaste_habib: asked.slice(0, 300), text: got.text, anjam_shod: got.anjam_shod, anjam_nashod: got.anjam_nashod, tedad_do_jomle: lines.length, tedad_abzar: tools.length, rah: 'fork', context_token: tokens })
594 return true
595 } catch {
596 return false
597 }
598}
599
600// returns whether the turn's summary (pj) was made in the same fork
601// «خودکار» goes to the chat by itself, the moment it comes (Habib, 26 Oct 03:06: «میخوام خودکار ها به صورت هوک
602// همون موقع که میان توی چت ایمپورت بشن و اگه انجام نشدن به صورت نکرده بیان -- و توی خودکار پنل هوک تیک فرستاده
603// شدن به چت بخورن»; 03:35: «هر خودکار یک بار و اگه انجام نشد یک بار هم نکرده مرتبط باهاش فرستاده بشه»): a turn of
604// its own ($.prompt.submit). Each one is sent once, and its «نکرده» once: that is what ends the chain. Per chat,
605// in ~/.claude/hads-log/<id>.khodkar_ferestade.json: {khodkar: [kar…], nakarde: [kar…]}
606const masir_khodkar_ferestade = async ($) => (await $.env.get('HOME')) + '/.claude/hads-log/' + (await $.session.id()) + '.khodkar_ferestade.json'
607const khodkar_ferestade_har_chat = new Map()
608const baraye_ferestadan_har_chat = new Map()
609async function khandan_khodkar_ferestade($) {
610 const id = await $.session.id()
611 if (!khodkar_ferestade_har_chat.has(id)) {
612 let o = null
613 try { o = JSON.parse(String(await $.fs.read(await masir_khodkar_ferestade($)))) } catch {}
614 const list = (x) => (Array.isArray(x) ? x.filter((k) => typeof k === 'string') : [])
615 const tedad = o?.tedad && typeof o.tedad === 'object' ? Object.fromEntries(Object.entries(o.tedad).filter(([, n]) => Number.isInteger(n))) : {}
616 khodkar_ferestade_har_chat.set(id, { khodkar: list(o?.khodkar), nakarde: list(o?.nakarde), rah: list(o?.rah), tedad })
617 }
618 return khodkar_ferestade_har_chat.get(id)
619}
620const kelid_rah = (x) => x.kar + ' ⇐ ' + x.abzar
621const dar_list = (list, kar) => list.some((k) => k === kar || hamun_matn(k, kar))
622// how many times one work went to the chat («خودکار» or «نکرده», any wording): the third time asks Habib, there is no
623// fourth (Habib, 26 Oct 07:50: «خودکاری که دوبار اومده و انجام نشده دفعه سوم هوک خودکار یه متنی اضافه کنه بهش که این مورد
624// رو اول به زبون ساده به حبیب توضیح بده و ازش بخواه کمکت کنه … و دفعه چهارم دیگه نفرستتش»)
625export const had_ferestadan_kar = 3
626export const matn_bar_sevom = ' (بارِ سوم است و هنوز انجام نشده: اول به زبانِ ساده به حبیب توضیح بده این کار چیست و کجا گیر کرده، و از او بخواه کمکت کند)'
627function tedad_kar(tedad, kar) {
628 const k = Object.keys(tedad).find((x) => x === kar || hamun_matn(x, kar))
629 return { kelid: k ?? kar, n: k ? tedad[k] : 0 }
630}
631export function matn_ferestadan_khodkar(kh, nk) {
632 const parts = ['خودکار (از مودِ «حدس»، به خواستِ حبیب: هر «خودکار» همان لحظه به چت میآید): اینها را خودت انجام بده و از من نپرس؛ نتیجهٔ هر کدام را خودت چک کن. اگر حبیب قبلاً دربارهاش حرفی زده (سرچ در چتهای قبلی)، طبقِ همان. اگر راهی که آمده نشد، منتظر نمان: از کارها و تجربههای قبلی یا ایجنتِ دیگری که ابزارش را دارد راهِ تازه پیدا کن و انجامش بده؛ فقط اگر خلافِ دستورِ حبیب است انجام نده و بگو کدام دستور.']
633 if (kh.length) parts.push(kh.map((x, i) => adad_farsi(i + 1) + '. ' + x.kar + ' ⇐ با ' + x.abzar + (x.chek ? '؛ چک: ' + x.chek : '') + (x.bar_sevom ? matn_bar_sevom : '')).join('\n'))
634 if (nk.length) parts.push('این «خودکار»ها قبلاً فرستاده شد و هنوز انجام نشده (نکرده):\n' + nk.map((x, i) => adad_farsi(i + 1) + '. ' + x.harf + (x.chera ? ' — ' + x.chera : '') + (x.bar_sevom ? matn_bar_sevom : '')).join('\n'))
635 return parts.join('\n\n')
636}
637// at the answer's end, after everything else: one message with the new «خودکار» and the «نکرده» of sent ones
638async function ferestadan_khodkar($) {
639 const id = await $.session.id()
640 const b = baraye_ferestadan_har_chat.get(id)
641 baraye_ferestadan_har_chat.delete(id)
642 if (!b || (!b.khodkar.length && !b.nakarde.length)) return
643 const f = await khandan_khodkar_ferestade($)
644 // once per work AND way: the same work with a new way (an excuse answered) goes again; a work sent before the
645 // ways were kept counts by its work alone
646 const qadimi = f.khodkar.filter((k) => !f.rah.some((r) => r.startsWith(k + ' ⇐ ')))
647 const kh0 = b.khodkar.filter((x) => !dar_list(f.rah, kelid_rah(x)) && !dar_list(qadimi, x.kar))
648 const nk0 = b.nakarde.filter((x) => !dar_list(f.nakarde, x.harf))
649 // the count: a work sent had_ferestadan_kar times does not go again; the last time carries the ask for Habib's help
650 const shomar = (list, kar_az) => list.map((x) => ({ x, t: tedad_kar(f.tedad, kar_az(x)) })).filter(({ t }) => t.n < had_ferestadan_kar)
651 .map(({ x, t }) => { f.tedad[t.kelid] = t.n + 1; return t.n + 1 === had_ferestadan_kar ? { ...x, bar_sevom: true } : x })
652 const kh = shomar(kh0, (x) => x.kar)
653 const nk = shomar(nk0, (x) => x.harf)
654 if (!kh.length && !nk.length) return
655 f.khodkar.push(...kh.map((x) => x.kar)); f.rah.push(...kh.map(kelid_rah)); f.nakarde.push(...nk.map((x) => x.harf))
656 try { await $.fs.write(await masir_khodkar_ferestade($), JSON.stringify(f) + '\n') } catch {}
657 await benevis_daftar($, 'khodkar_ferestade', { khodkar_ha: kh, nakarde_ha: nk.map((x) => x.harf) })
658 $.ui.invalidate('ui.render')
659 try { await $.prompt.submit({ text: matn_ferestadan_khodkar(kh, nk) }) } catch {}
660}
661
662function baraye_ferestadan($, k, list) {
663 return $.session.id().then((id) => {
664 const b = baraye_ferestadan_har_chat.get(id) || { khodkar: [], nakarde: [] }
665 b[k] = list; baraye_ferestadan_har_chat.set(id, b)
666 })
667}
668async function hads_ba_fork($, p = { hads: true }, pj = null) {
669 let got = null
670 let jambandi_shod = false
671 let tokens = 0
672 try { tokens = (await $.session.usage()).context.tokens || 0 } catch {}
673 // «نکردهها», «خودکار», «کرونجاب» and the summary need the whole chat: only the chat's own fork
674 // has it, so a big chat forks for them too
675 if (tokens <= had_fork_context_token || p.nakarde || p.khodkar || p.cron || pj) {
676 try {
677 const ferestade = await khandan_khodkar_ferestade($)
678 const khaste = { hads: !!p.hads, nakarde: !!p.nakarde, khodkar: !!p.khodkar, cron: !!p.cron, commit_ha: p.nakarde ? await commit_ha_ye_akhir($) : '', khodkar_ferestade: p.nakarde ? ferestade.khodkar.slice(-20) : [], khodkar_rah: p.khodkar ? ferestade.rah.slice(-20) : [] }
679 const anjam = khaste.nakarde ? await khandan_nakarde_anjam($) : []
680 const r = await $.model.fork({ prompt: pj ? prompt_jambandi_nobat(pj.lines, pj.tools, khaste, anjam) : prompt_fork(khaste, anjam) })
681 got = r.isAnswered ? khandan_javab_fork(r.text) : null
682 if (got?.hads.length) try { await $.store.set('kharj_akhar_hads_fork', { rah: 'fork', context_token: tokens, usage: r.usage }) } catch {}
683 if (r.isAnswered) await benevis_kharj($, [pj && 'jambandi_nobat', khaste.hads && 'hads', khaste.nakarde && 'nakarde', khaste.khodkar && 'khodkar', khaste.cron && 'cron'].filter(Boolean).join('_va_'), 'fork', r.usage)
684 if (pj) jambandi_shod = await sabt_jambandi($, pj, r.isAnswered ? r.text : '')
685 } catch {}
686 } else if (pj) jambandi_shod = await jambandi($, pj)
687 if (p.nakarde && got) {
688 // a «نکرده» its own check finds done (the file has it, a commit says it) is not shown, and is logged
689 const ba_sanad = []
690 for (const x of got.nakarde) if (await sanad_dare($, x.sanad)) ba_sanad.push(x)
691 if (ba_sanad.length) await benevis_daftar($, 'nakarde_ba_sanad', { anjam_shode: ba_sanad.map((x) => ({ harf: x.harf, sanad: x.sanad })) })
692 nakarde_ba_sanad_akhar = ba_sanad
693 const ferestade = (await khandan_khodkar_ferestade($)).khodkar
694 nakarde_akhar = hanuz_nakarde(got.nakarde.filter((x) => !ba_sanad.includes(x)), await khandan_nakarde_anjam($))
695 .map((x) => (dar_list(ferestade, x.harf) ? { ...x, khodkar: true } : x)); hads_baraye_chat = await $.session.id()
696 if (nakarde_akhar.some((x) => x.khodkar)) await baraye_ferestadan($, 'nakarde', nakarde_akhar.filter((x) => x.khodkar))
697 await benevis_daftar($, 'nakarde', { rah: 'fork', nakarde_ha: nakarde_akhar })
698 $.ui.invalidate('ui.render')
699 }
700 if (p.cron && got) {
701 cron_akhar = got.cron; hads_baraye_chat = await $.session.id()
702 await benevis_daftar($, 'cron', { rah: 'fork', cron_ha: cron_akhar })
703 $.ui.invalidate('ui.render')
704 }
705 if (p.khodkar && got) {
706 khodkar_akhar = got.khodkar; hads_baraye_chat = await $.session.id()
707 if (khodkar_akhar.length) await baraye_ferestadan($, 'khodkar', khodkar_akhar)
708 await benevis_daftar($, 'khodkar', { rah: 'fork', khodkar_ha: khodkar_akhar })
709 $.ui.invalidate('ui.render')
710 }
711 if (!p.hads) return jambandi_shod
712 if (got?.hads.length) {
713 hads_akhar = got.hads; hads_baraye_chat = await $.session.id()
714 await benevis_daftar($, 'hads', { rah: 'fork', hads_ha: hads_akhar.map((g) => g.text) })
715 $.ui.invalidate('ui.render')
716 } else await hads_ba_sonnet($)
717 return jambandi_shod
718}
719
720const matn_porsesh_montazer = 'این پرسش بعد از جوابِ بعدیِ کلود در همین چت خودش پرسیده میشود (بعد از resume یا وسطِ کار، تا اولین جواب چیزی برای خواندن نیست).'
721const matn_dalil_bi_javab = {
722 'nothing-to-fork': 'این چت هنوز چیزی ندارد (مثلاً تازه /clear شده)؛ اول یک پیام بفرست.',
723 'api-error': 'خطا از سمتِ Claude؛ دوباره بپرس.',
724 'empty-reply': 'جوابی نیامد؛ دوباره بپرس.',
725 aborted: 'نیمهکاره ماند؛ دوباره بپرس.',
726}
727const masir_prompt_ha = async ($) => (await $.env.get('HOME')) + '/.claude/hads-prompt-ha.json'
728async function khandan_prompt_ha($) {
729 if (prompt_ha) return prompt_ha
730 let o = null
731 try { o = JSON.parse(String(await $.fs.read(await masir_prompt_ha($)))) } catch {}
732 prompt_ha = Array.isArray(o) ? o.filter((x) => typeof x === 'string' && x.trim()) : []
733 return prompt_ha
734}
735async function zakhire_prompt_ha($) {
736 try { await $.fs.write(await masir_prompt_ha($), JSON.stringify(prompt_ha) + '\n') } catch {}
737 $.ui.invalidate('ui.render')
738}
739export const had_harf_dokme_prompt = 18
740async function zakhire_prompt_virayesh($) {
741 const v = prompt_virayesh
742 if (!v) return
743 const matn = String(v.matn || '').trim()
744 prompt_virayesh = null
745 await khandan_prompt_ha($)
746 if (!matn) return $.ui.invalidate('ui.render')
747 if (v.i >= 0 && v.i < prompt_ha.length) prompt_ha[v.i] = matn; else prompt_ha.push(matn)
748 return zakhire_prompt_ha($)
749}
750async function prompt_entekhab_shode($) {
751 const i = prompt_entekhab_har_chat.get(await $.session.id())
752 const list = await khandan_prompt_ha($)
753 return i != null && list[i] ? list[i] : ''
754}
755async function porsesh_kon($, q) {
756 if (!String(q).trim()) return
757 // off in this chat, or «پرسش» unticked: nothing would be drawn, so no fork is paid for
758 if (!(await khandan_file_chat($))?.porsesh) return
759 const id = await $.session.id()
760 // a cold cache re-reads the whole chat at the write price: say how much, ask Enter again.
761 // Known only from this process's own activity or the resume's numbers; no word means no warning
762 // is claimed as fact.
763 const idle = zaman_akharin_kar ? (await $.clock.now()) - zaman_akharin_kar : -1
764 const cold = cache_sard_shod || idle > cache_sard_bad_az_ms
765 if ((cold || idle < 0) && !(javab_porsesh?.cold && javab_porsesh.q === q && javab_porsesh.for === id)) {
766 let tokens = 0, model = ''
767 try { tokens = (await $.session.usage()).context.tokens || 0 } catch {}
768 try { model = await $.session.model() } catch {}
769 if (tokens > 0) {
770 const [a, b] = kharj_cache_sard(model, tokens)
771 const cost = '$' + a.toFixed(2) + ' تا $' + b.toFixed(2) + ' (' + Math.round(tokens / 1000) + 'k توکن)'
772 javab_porsesh = { q, for: id, cold: true,
773 text: (cold ? 'کشِ چت سرد است (' + Math.round(idle / 60000) + ' دقیقه بیکار' + (cache_sard_shod ? '، به گفتهٔ خودِ Claude Code' : '') + '): این سؤال حدودِ ' + cost + ' خرج دارد.'
774 : 'نمیدانم کشِ چت گرم است یا نه؛ اگر سرد باشد این سؤال حدودِ ' + cost + ' خرج دارد.') + ' برای پرسیدن «باشه، بپرس» یا دوباره Enter را بزن.' }
775 $.ui.invalidate('ui.render')
776 return
777 }
778 }
779 // this question's own box: × or a newer question may replace `answer` while the fork runs
780 const cur = javab_porsesh = { q, text: '…', for: id }; matn_porsesh = ''; $.ui.invalidate('ui.render')
781 try {
782 // mid-work the fork sees a tool call in flight and may try to go on with it: say it may not
783 const pr = await prompt_entekhab_shode($)
784 const r = await $.model.fork({ prompt: (pr ? 'پرومپتِ حبیب برای این پرسش (طبقِ همین جواب بده):\n' + pr + '\n\nپرسش:\n' : '') + q + '\n\n(این پرسش از کادرِ حدس است، نه دستورِ کار: ابزاری صدا نزن و کاری را ادامه نده؛ فقط کوتاه، به فارسیِ ساده و با واژههای حبیب جواب بده.)' })
785 // nothing to read yet, though the chat has a story: wait for the next answer, then ask
786 let has = false
787 if (!r.isAnswered && r.reason === 'nothing-to-fork') {
788 try { has = ((await $.session.usage()).context.tokens || 0) > 0 || (await $.session.messages()).length > 0 } catch {}
789 }
790 if (has) { porsesh_montazer = q; cur.text = matn_porsesh_montazer; cur.waiting = true; await benevis_daftar($, 'porsesh_montazer', { porsesh_matn: q }) }
791 else cur.text = r.isAnswered ? r.text : (matn_dalil_bi_javab[r.reason] || 'جوابی نیامد (' + r.reason + ')؛ دوباره بپرس.')
792 // the fork just read the whole chat into the cache: it is warm for the next question
793 if (r.isAnswered) { try { zaman_akharin_kar = await $.clock.now() } catch {} cache_sard_shod = false }
794 if (r.isAnswered) await benevis_daftar($, 'porsesh', { porsesh_matn: q, javab_matn: r.text, rah: 'fork' })
795 if (r.isAnswered) try { await $.store.set('kharj_akhar_porsesh', r.usage) } catch {}
796 if (r.isAnswered) await benevis_kharj($, 'porsesh', 'fork', r.usage)
797 } catch {
798 cur.text = matn_dalil_bi_javab.aborted
799 }
800 if (javab_porsesh === cur) $.ui.invalidate('ui.render')
801}
802
803// No app attached (claude -p, SDK) means nobody sees the row: skip the model call.
804async function chat_ba_safhe($) {
805 try {
806 const s = await $.session.surfaces()
807 return !Array.isArray(s) || s.length > 0
808 } catch {
809 return true
810 }
811}
812
813// Keep what is already typed: the guess goes on its own line after it.
814async function gozashtan_dar_kadr($, text) {
815 let cur = ''
816 try {
817 const v = await $.prompt.read()
818 cur = typeof v === 'string' ? v : String(v?.text ?? '')
819 } catch {}
820 return $.prompt.fill({ text: cur.trim() ? cur + '\n' + text : text })
821}
822
823// on/off is per chat: one file per session id (~/.claude/hads-on/<id>), so no chat can
824// switch another, not even one in the same folder. One file each, not $.store: the store
825// is one JSON file every open chat rewrites. A new id (/clear, a new chat) starts off.
826const masir_file_chat = async ($) => (await $.env.get('HOME')) + '/.claude/hads-on/' + (await $.session.id())
827// The file holds `on`, then the parts ticked off: `on -hads -porsesh`. h-on turns all of them on.
828export const bakhsh_ha = [['hads', 'حدس'], ['nakarde', 'نکردهها'], ['khodkar', 'خودکار'], ['cron', 'کرونجاب'], ['do_jomle', 'دو جمله'], ['porsesh', 'پرسش'], ['hook', 'هوکها']]
829// every word the chat's file may hold, written out whole so grep finds each one (تعریفِ ۱۰: no
830// name glued from pieces): a part ticked off, and the other settings
831export const esm_bakhsh = Object.fromEntries(bakhsh_ha)
832// «🔎 حافظه»: how wide this chat's memory search reaches (Habib to ۲۰۵, 25 Oct 18:03: «این قابلیت سرچ کردن
833// باید قابل تنظیم باشه که … فقط از توی چت خودش باشه یا از توی چتای توی پروژه خودش باشه یا از توی کل چتای
834// موجود در کل پروژه ها»; built here, 18:30: «بله، بسازید»). ۲۰۵ owns the file (hafeze_tanzim.json): its
835// command writes and reads it, the label shows what its `get` says, a press moves only this chat one on
836export const dayere_ha = ['chat', 'poshe', 'hame']
837export const esm_dayere = { chat: 'همین چت', poshe: 'همین پروژه', hame: 'همهٔ پروژهها' }
838const masir_hafeze_dayere = async ($) => (await $.env.get('HOME')) + '/1-MY-WORK/205th-day-mother-of-all-agent/abzar/hafeze_dayere.py'
839// per chat id: what `get` last said (undefined: not asked yet; null: no answer)
840const dayere_har_chat = new Map()
841async function hafeze_dayere($, ...args) {
842 try {
843 // the chat's own folder too: a chat its memory has not indexed yet is not known by id alone, and the
844 // folder layer would be missed (۲۰۵, 19:01: in 203's folder, get without it said poshe, the hook used chat)
845 let poshe = ''
846 try { poshe = await $.session.root() } catch {}
847 const r = await $.process.run([await masir_hafeze_dayere($), ...args, '--chat', await $.session.id(), ...(poshe ? ['--poshe', poshe] : [])], { timeoutMs: 15000 })
848 const o = JSON.parse(String(r.stdout || '').trim().split('\n').pop())
849 return dayere_ha.includes(o?.dayere) ? o.dayere : null
850 } catch {
851 return null
852 }
853}
854async function khandan_dayere($) {
855 const id = await $.session.id()
856 dayere_har_chat.set(id, await hafeze_dayere($, 'get'))
857 $.ui.invalidate('ui.render')
858}
859async function taghir_dayere($, be) {
860 const id = await $.session.id()
861 const cur = dayere_har_chat.get(id)
862 const next = be || dayere_ha[(dayere_ha.indexOf(cur) + 1) % dayere_ha.length]
863 await hafeze_dayere($, 'set', next)
864 // the label is what get says after the write, never what was asked for
865 await khandan_dayere($)
866 await benevis_daftar($, 'hafeze_dayere', { khaste: next, hast: dayere_har_chat.get(id) })
867 return dayere_har_chat.get(id)
868}
869// What the box shows, kept per chat in ~/.claude/hads-log/<id>.akharin.json, so a reload of VS Code (a new
870// process, every variable empty) shows it again (Habib, 25 Oct 18:09: «وقتی که VS Code رو ریلود میکنیم اون
871// متنایی که تو پنجره هدس هست اونا همه پاک میشه و صفر میشه. بعد از قبلی ها توش باشه»). Written when it
872// changes, read back once per chat; «قبلیها» have their own file and come back with «دو جمله».
873const masir_akharin = async ($) => (await $.env.get('HOME')) + '/.claude/hads-log/' + (await $.session.id()) + '.akharin.json'
874const tasvir_akharin = (id) => JSON.stringify({
875 do_jomle: do_jomle_akhar.for === id && do_jomle_akhar.text ? { text: do_jomle_akhar.text, summary: do_jomle_akhar.summary || null } : null,
876 ...(hads_baraye_chat === id ? { hads: hads_akhar, nakarde: nakarde_akhar, nakarde_ba_sanad: nakarde_ba_sanad_akhar, khodkar: khodkar_akhar, cron: cron_akhar } : {}),
877 porsesh: javab_porsesh && javab_porsesh.for === id && !javab_porsesh.cold && !javab_porsesh.waiting && javab_porsesh.text !== '…' ? { q: javab_porsesh.q, text: javab_porsesh.text } : null,
878})
879const akharin_zakhire = new Map()
880async function zakhire_akharin($, id) {
881 const t = tasvir_akharin(id)
882 if (akharin_zakhire.get(id) === t) return
883 akharin_zakhire.set(id, t)
884 try { await $.fs.write(await masir_akharin($), t + '\n') } catch {}
885}
886const akharin_bargashte = new Set()
887async function bargardandan_akharin($, id) {
888 if (akharin_bargashte.has(id)) return
889 akharin_bargashte.add(id)
890 let o = null
891 try { o = JSON.parse(String(await $.fs.read(await masir_akharin($)))) } catch {}
892 if (o?.do_jomle?.text && do_jomle_akhar.for !== id) {
893 const sm = o.do_jomle.summary ? khandan_jambandi(JSON.stringify(o.do_jomle.summary)) : null
894 do_jomle_akhar = { text: String(o.do_jomle.text), for: id, ...(sm ? { summary: sm } : {}) }
895 }
896 if (o && hads_baraye_chat !== id && (o.hads || o.nakarde || o.khodkar || o.cron)) {
897 hads_akhar = khandan_hads(JSON.stringify(o.hads || []))
898 nakarde_akhar = hanuz_nakarde(khandan_nakarde(o.nakarde), await khandan_nakarde_anjam($))
899 nakarde_ba_sanad_akhar = khandan_nakarde(o.nakarde_ba_sanad)
900 khodkar_akhar = khandan_khodkar(o.khodkar); cron_akhar = khandan_cron(o.cron); hads_baraye_chat = id
901 }
902 if (o?.porsesh?.text && !javab_porsesh) javab_porsesh = { q: String(o.porsesh.q || ''), text: String(o.porsesh.text), for: id }
903}
904// the order of the parts, the same in every chat (Habib, 25 Oct 18:01: «این دکمه ها باید قابلیت جابجایی
905// داشته باشن و با جابجا شدن ردیفشون عوض بشه. یعنی اگه حدس اوله میشه ردیف یک»): one line of part
906// names in ~/.claude/hads-tartib; a part it lacks comes after, in bakhsh_ha's order
907export function khandan_tartib(text) {
908 const kol = bakhsh_ha.map(([k]) => k)
909 const ba = [...new Set(String(text || '').trim().split(/\s+/).filter((w) => kol.includes(w)))]
910 return [...ba, ...kol.filter((k) => !ba.includes(k))]
911}
912const masir_tartib = async ($) => (await $.env.get('HOME')) + '/.claude/hads-tartib'
913async function khandan_tartib_file($) {
914 try { return khandan_tartib(String(await $.fs.read(await masir_tartib($)))) } catch { return khandan_tartib('') }
915}
916async function taghir_tartib($, v) {
917 const tazeh = khandan_tartib(v)
918 if (tazeh.join(' ') === (await khandan_tartib_file($)).join(' ')) return
919 try { await $.fs.write(await masir_tartib($), tazeh.join(' ') + '\n') } catch {}
920 $.ui.invalidate('ui.render')
921}
922export const kalame_khamush = { hads: '-hads', nakarde: '-nakarde', khodkar: '-khodkar', cron: '-cron', do_jomle: '-do_jomle', porsesh: '-porsesh', hook: '-hook' }
923export const kalame_tanzim = { do_jomle_fork: '+do_jomle_fork', yek_khat: 'yek_khat', tak_dokme: 'tak_dokme', hook_bozorg: 'hook_bozorg' }
924async function khandan_file_chat($) {
925 try {
926 const w = String(await $.fs.read(await masir_file_chat($))).trim().split(/\s+/)
927 if (w[0] !== 'on') return null
928 return { ...Object.fromEntries(bakhsh_ha.map(([k]) => [k, !w.includes(kalame_khamush[k])])),
929 ...Object.fromEntries(Object.entries(kalame_tanzim).map(([k, kalame]) => [k, w.includes(kalame)])) }
930 } catch {
931 return null
932 }
933}
934const roshan_ast = async ($) => (await khandan_file_chat($)) !== null
935const kalame_ha_ye_file = (p) => ['on', ...bakhsh_ha.filter(([k]) => !p[k]).map(([k]) => kalame_khamush[k]), ...Object.entries(kalame_tanzim).filter(([k]) => p[k]).map(([, kalame]) => kalame)].join(' ')
936// ▾ / ▸: the whole band folds to one line and stays so after a reload (the word `yek_khat`)
937// three states (Habib, 24 Oct 22:52: «سه حالته، یکی حالت باز، یکی حالت مثل الان … یه حالتی هم که
938// بشه اصلا یه دکمه»): open (baz) · folded to one line (yek_khat) · just one small button (tak_dokme).
939// The button opens the folded line; the line opens the box. Kept in the chat's file over reloads.
940async function taghir_halat($, mode) {
941 const p = await khandan_file_chat($)
942 if (!p) return
943 p.yek_khat = mode === 'yek_khat'; p.tak_dokme = mode === 'tak_dokme'
944 await $.fs.write(await masir_file_chat($), kalame_ha_ye_file(p))
945 $.ui.invalidate('ui.render')
946}
947const jam_kardan = ($) => khandan_file_chat($).then((p) => p && taghir_halat($, p.yek_khat || p.tak_dokme ? 'baz' : 'tak_dokme'))
948let ghablia_baz = false
949
950// «هوکها»: the VS Code panel drops every hook_* message on purpose (extension.js filters
951// subtype hook_ out, the webview has no view for them), so the band shows what the hooks
952// injected, read from the transcript the CLI writes: the attachments of type hook_success,
953// hook_additional_context and hook_system_message since the last prompt, and the last
954// SessionStart's. A transcript passes $.fs.read's 4 MiB and its tail is mostly request blobs,
955// so grep keeps only hook attachments and typed prompts, and of those the last had_hook_transcript_khat lines.
956export const had_hook_transcript_khat = 400
957export const had_hook_transcript_byte = 4000000
958export const hook_transcript_grep = '"attachment":\\{"type":"hook_|"type":"user","message":\\{"role":"user","content":("|\\[\\{"type":"text")'
959// one line each: the Shamsi date and time first (~۲۰ characters), then the hook
960export const had_hook_kuchak_harf = 100
961let masir_transcript = ''
962let hook_ha = { list: [], for: '' }
963let zaman_khandan_hook = 0
964export function khandan_hook_ha(text) {
965 const lines = String(text || '').split('\n')
966 const all = []
967 let lastPrompt = -1
968 for (const l of lines) {
969 let o
970 try { o = JSON.parse(l) } catch { continue }
971 const a = o.attachment
972 if (a && typeof a === 'object' && typeof a.type === 'string' && a.type.startsWith('hook_')) {
973 const body = Array.isArray(a.content) ? a.content.join('\n') : String(a.content ?? '')
974 if (body.trim() && ['hook_success', 'hook_additional_context', 'hook_system_message'].includes(a.type)) {
975 const t = Date.parse(o.timestamp)
976 all.push({ name: a.hookName || a.hookEvent || a.type, kind: a.type, text: body.trim(), t: Number.isFinite(t) ? t : 0 })
977 }
978 continue
979 }
980 // a prompt the person (or another chat) sent, not a tool result
981 if (o.type === 'user' && !o.isMeta) {
982 const c = o.message?.content
983 const typed = typeof c === 'string' || (Array.isArray(c) && c.some((b) => b?.type === 'text'))
984 if (typed) lastPrompt = all.length
985 }
986 }
987 const since = lastPrompt < 0 ? all : all.slice(lastPrompt)
988 const start = all.filter((h) => /^SessionStart/.test(h.name)).slice(-3)
989 // the same hook with the same text (a reminder after every Bash) is one line with ×n
990 const out = []
991 for (const h of [...start.filter((x) => !since.includes(x)), ...since]) {
992 const twin = out.find((x) => x.name === h.name && x.text === h.text)
993 // a repeated hook keeps the time of its latest run
994 if (twin) { twin.n++; twin.t = Math.max(twin.t, h.t) }
995 else out.push({ ...h, n: 1 })
996 }
997 return out
998}
999// on screen: the last had_hook_kuchak_radif of them
1000export const had_hook_kuchak_radif = 6
1001export const had_hook_bozorg_radif = 3
1002export const had_hook_bozorg_harf = 600
1003async function khandan_hook_az_transcript($) {
1004 try {
1005 const t = await $.clock.now()
1006 if (t - zaman_khandan_hook < 2000) return
1007 zaman_khandan_hook = t
1008 let path = masir_transcript
1009 if (!path) {
1010 const slug = String(await $.session.cwd()).replace(/[^a-zA-Z0-9]/g, '-')
1011 path = (await $.env.get('HOME')) + '/.claude/projects/' + slug + '/' + (await $.session.id()) + '.jsonl'
1012 }
1013 // only the file's last had_hook_transcript_byte are searched: a long chat's transcript is hundreds of MB,
1014 // and this runs after tools. The first line of the cut is half a line; parseHooks skips it.
1015 const r = await $.process.run(['sh', '-c', 'tail -c "$4" "$2" | grep -E "$1" | tail -n "$3"', 'sh', hook_transcript_grep, path, String(had_hook_transcript_khat), String(had_hook_transcript_byte)])
1016 if (r.exitCode !== 0) return
1017 hook_ha = { list: khandan_hook_ha(r.stdout), for: await $.session.id() }
1018 $.ui.invalidate('ui.render')
1019 } catch {}
1020}
1021// no emoji in «هوکها» either, ours or the hook's own (Habib, 26 Oct 05:36: «این ایموجی های مزخرف رو کامل بردار - کامل بدون ایموجی»)
1022export const bi_emoji = (t) => String(t).replace(/(?:\p{Extended_Pictographic}|[\u{1F1E6}-\u{1F1FF}])[\uFE0F\u200D]*/gu, '').replace(/[ \t]{2,}/g, ' ').trim()
1023const khat_hook = (h) => bi_emoji((h.t ? zaman_shamsi(h.t) + ' · ' : '') + h.name + (h.n > 1 ? ' ×' + adad_farsi(h.n) : '') + ' · ' + h.text.replace(/\s+/g, ' '))
1024// a hook's message beside the text it injected: only the injected text («فقط اون دومی رو نشون بده … اولی نیاد»)
1025export const bi_payam_tekrari = (list) => list.filter((h) => !(h.kind === 'hook_system_message' && list.some((x) => x.kind === 'hook_additional_context' && x.name === h.name && Math.abs((x.t || 0) - (h.t || 0)) <= 10000)))
1026async function tik_zadan($, key) {
1027 const p = await khandan_file_chat($)
1028 if (!p) return
1029 p[key] = !p[key]
1030 await $.fs.write(await masir_file_chat($), kalame_ha_ye_file(p))
1031 if (!p.hads) hads_akhar = []
1032 if (!p.nakarde) { nakarde_akhar = []; nakarde_ba_sanad_akhar = [] }
1033 if (!p.khodkar) khodkar_akhar = []
1034 if (!p.cron) cron_akhar = []
1035 if (!p.do_jomle) do_jomle_akhar = { text: '', for: '' }
1036 if (!p.porsesh) { javab_porsesh = null; porsesh_montazer = null }
1037 $.ui.invalidate('ui.render')
1038}
1039
1040export function register(on) {
1041 on('session.start', async ($, e, next) => {
1042 try {
1043 await $.command.register({ name: 'hads', description: 'حدس: on | off | now | ask <پرسش> | hafeze chat|poshe|hame', argumentHint: '[on|off|now|ask …|hafeze …]', immediate: true })
1044 } catch {}
1045 return next(e)
1046 })
1047
1048 // resume / fork: Claude Code says how long the chat was idle and whether its cache expired
1049 on('classic.SessionStart', async ($, e, next) => {
1050 try {
1051 if (typeof e.transcript_path === 'string') masir_transcript = e.transcript_path
1052 if (typeof e.seconds_since_last_response === 'number') zaman_akharin_kar = (await $.clock.now()) - e.seconds_since_last_response * 1000
1053 cache_sard_shod = e.prompt_cache_likely_expired === true
1054 } catch {}
1055 return next(e)
1056 })
1057 // a model switch tells the TTL; the new model's cache starts cold
1058 on('classic.PostModelSwitch', async ($, e, next) => {
1059 try {
1060 cache_sard_bad_az_ms = e.cache_ttl === '5m' ? cache_sard_5m_ms : cache_sard_ms
1061 if (e.from_model !== e.to_model) cache_sard_shod = true
1062 } catch {}
1063 return next(e)
1064 })
1065
1066 on('command.run', { command: 'hads' }, async ($, e) => {
1067 const arg = (e.args || '').trim()
1068 if (arg === 'off' || arg === 'on') {
1069 await $.fs.write(await masir_file_chat($), arg)
1070 if (arg === 'off') { hads_akhar = []; do_jomle_akhar = { text: '', for: '' }; javab_porsesh = null; porsesh_montazer = null }
1071 // nothing to tell yet: say when it comes (no fork here, the cache may be cold)
1072 else { do_jomle_akhar = { text: 'حدس روشن شد؛ بعد از جوابِ بعدی دو جمله و دکمهها میآید.', for: await $.session.id() } }
1073 $.ui.invalidate('ui.render')
1074 return { text: 'حدس برای همین چت ' + (arg === 'off' ? 'خاموش شد' : 'روشن شد') }
1075 }
1076 // works with the box off too
1077 if (arg.startsWith('hafeze')) {
1078 const be = arg.slice(6).trim()
1079 if (be && !dayere_ha.includes(be)) return { text: 'یکی از اینها: /hads hafeze chat | poshe | hame' }
1080 const hast = be ? await taghir_dayere($, be) : (await khandan_dayere($), dayere_har_chat.get(await $.session.id()))
1081 return { text: hast ? 'دایرهٔ سرچِ حافظه برای همین چت: ' + esm_dayere[hast] : 'فرمانِ حافظهٔ ۲۰۵ جواب نداد' }
1082 }
1083 if (arg.startsWith('ask ') && !(await khandan_file_chat($))?.porsesh) return { text: 'پرسش در این چت خاموش است' }
1084 if (arg.startsWith('ask ')) { $.clock.after(0, () => porsesh_kon($, arg.slice(4).trim())); return { text: 'پرسش رفت؛ جوابش در کادرِ حدس میآید.' } }
1085 // /hads now: asked for, so the guesses come now whatever the count
1086 const pn = await khandan_file_chat($)
1087 if (!pn) return { text: 'حدس در این چت خاموش است؛ اول /hads on' }
1088 $.clock.after(0, () => akhar_javab($, pn, true))
1089 return { text: 'حدس در حال ساختن…' }
1090 })
1091
1092 // the prompt's own hooks are written right after it: read them a moment later
1093 // a guess Habib pressed into the box and then sent (as is, or edited): his own message now
1094 on('prompt.submit', async ($, e, next) => {
1095 try { const sid = await $.session.id(); do_jomle_ha_ye_nobat.set(sid, []); abzar_ha_ye_nobat.set(sid, []); khaste_habib_nobat.set(sid, harf_habib(String(e?.text || '').trim())) } catch {}
1096 try {
1097 const sent = String(e?.text || '').trim()
1098 if (sent && hads_zade_shode.length) {
1099 const hit = hads_zade_shode.find((g) => sent.includes(g)) || hads_zade_shode.find((g) => hamun_matn(g, sent))
1100 if (hit) await benevis_daftar($, 'hads_ferestade', { hads_matn: hit, matn_ferestade: sent, virayesh_shod: !sent.includes(hit) })
1101 hads_zade_shode = []
1102 }
1103 } catch {}
1104 return next(e)
1105 })
1106
1107 on('classic.UserPromptSubmit', async ($, e, next) => {
1108 try {
1109 if (typeof e.transcript_path === 'string') masir_transcript = e.transcript_path
1110 $.clock.after(1500, async () => { if ((await khandan_file_chat($))?.hook) { zaman_khandan_hook = 0; await khandan_hook_az_transcript($) } })
1111 } catch {}
1112 return next(e)
1113 })
1114
1115 on('turn.complete', async ($, e, next) => {
1116 if (!e.agentId) {
1117 try { zaman_akharin_kar = await $.clock.now(); cache_sard_shod = false } catch {}
1118 // a «پرسش» that waited for this answer goes now
1119 if (porsesh_montazer && !e.isAborted) { const q = porsesh_montazer; porsesh_montazer = null; $.clock.after(0, () => porsesh_kon($, q)) }
1120 }
1121 const p = !e.agentId && !e.isAborted && (await chat_ba_safhe($)) ? await khandan_file_chat($) : null
1122 if (p?.hook) $.clock.after(500, () => { zaman_khandan_hook = 0; return khandan_hook_az_transcript($) })
1123 // not awaited: the turn must not wait for the model
1124 if (p) $.clock.after(0, () => akhar_javab($, p))
1125 return next(e)
1126 })
1127
1128 // after every tool: the tool runs first and its result goes back untouched; the rest
1129 // is never awaited and never throws, so «دو جمله» can not slow or break a tool
1130 on('tool.call', async ($, e, next) => {
1131 const r = await next(e)
1132 try {
1133 if (!e.agentId) {
1134 const what = e.tool + ': ' + String(e.description || e.command || e.file_path || '').slice(0, 200)
1135 // remembered for the turn's summary (a short line each, at most 30)
1136 $.clock.after(0, async () => { try { const sid = await $.session.id(); abzar_ha_ye_nobat.set(sid, [...(abzar_ha_ye_nobat.get(sid) || []), what.slice(0, 120)].slice(-30)) } catch {} })
1137 $.clock.after(0, async () => {
1138 try {
1139 zaman_akharin_kar = await $.clock.now(); cache_sard_shod = false
1140 const p = (await chat_ba_safhe($)) ? await khandan_file_chat($) : null
1141 if (p?.hook) await khandan_hook_az_transcript($)
1142 if (p?.do_jomle) await begu_do_jomle($, what, false, p.do_jomle_fork && (await chat_kuchak($)))
1143 } catch {}
1144 })
1145 }
1146 } catch {}
1147 return r
1148 })
1149
1150 on('ui.render', { component: 'AbovePrompt' }, async ($, e, next) => {
1151 const p = await khandan_file_chat($)
1152 if (!p) return next(e)
1153 const id = await $.session.id()
1154 await bargardandan_akharin($, id)
1155 zakhire_akharin($, id)
1156 if (!dayere_har_chat.has(id)) { dayere_har_chat.set(id, null); khandan_dayere($) }
1157 const g = p.hads && hads_baraye_chat === id ? hads_akhar : []
1158 const nk = p.nakarde && hads_baraye_chat === id ? nakarde_akhar : []
1159 const nks = p.nakarde && hads_baraye_chat === id ? nakarde_ba_sanad_akhar : []
1160 const kh = p.khodkar && hads_baraye_chat === id ? khodkar_akhar : []
1161 const cr = p.cron && hads_baraye_chat === id ? cron_akhar : []
1162 const n = p.do_jomle && do_jomle_akhar.for === id ? do_jomle_akhar.text : ''
1163 const sm = n && do_jomle_akhar.summary ? do_jomle_akhar.summary : null
1164 const a = p.porsesh && javab_porsesh && javab_porsesh.for === id ? javab_porsesh : null
1165 const { Box, Text, Button, Input } = $.ui.resolve(e)
1166 const theirs = await next(e)
1167 // folded: one line, ▸ opens it again
1168 // one small button only: it opens the folded line
1169 // two states now: the folded line is gone, the button opens the whole box and × goes straight back to it
1170 // (Habib, 26 Oct 19:18: «کلا این مرحله حذف بشه بعد از زدن اون دکمه کامل باز بشه» · «همدو باز کردن هم تو بستن»);
1171 // a file still holding the old word `yek_khat` shows the button too
1172 if (p.tak_dokme || p.yek_khat) {
1173 const one = Button({ key: 'hads-dot', label: 'حدس ▸', onPress: () => taghir_halat($, 'baz') })
1174 return Box({ flexDirection: 'column', children: theirs ? [theirs, one] : [one] })
1175 }
1176 const hist = n || ghablia_baz ? await khandan_ghablia($) : []
1177 const prompt_list = p.porsesh ? await khandan_prompt_ha($) : []
1178 const prompt_i = prompt_entekhab_har_chat.get(id) ?? null
1179 // ▾ (one line) and × (one button) on top, where they are always in sight however long the box
1180 // grows (Habib, 24 Oct 22:41: «اینها باید بالای صفحه باشند که همه جا قابلیت دیدن داشته باشند»).
1181 // The box never closes all the way: × leaves the one button (25 Oct 00:13: «ضربدر … تک دکمه ای
1182 // شدن یعنی کلا هیچوقت بسته نشه … همین سه تا نباشه»)
1183 const rows = [Box({ key: 'hads-top', flexDirection: 'row', columnGap: 1, justifyContent: 'flex-end', children: [
1184 Button({ key: 'hads-x', label: '×', onPress: () => taghir_halat($, 'tak_dokme') }),
1185 ] })]
1186 // each part's rows, laid out below in Habib's order (the ticks' order): ▾ × stay on top, the ticks last
1187 const goruh = Object.fromEntries(bakhsh_ha.map(([k]) => [k, []]))
1188 const ferestade = kh.length || nk.length ? await khandan_khodkar_ferestade($) : { khodkar: [], nakarde: [] }
1189 const tartib = await khandan_tartib_file($)
1190 // plain text, not buttons: Habib selects it with the mouse and copies it (Ctrl+C);
1191 // the panel's rows are user-select:text in VS Code
1192 if (n) goruh.do_jomle.push(Box({ flexDirection: 'row', columnGap: 1, children: [
1193 Text({ dimColor: true, children: [sm ? 'جمعبندی:' : 'دو جمله:'] }),
1194 Text({ key: 'hads-now', children: [n] }),
1195 ...(hist.length > 1 ? [Button({ key: 'hads-hist', label: (ghablia_baz ? '▴ ' : '') + 'قبلیها ' + adad_farsi(hist.length - 1), onPress: () => { ghablia_baz = !ghablia_baz; $.ui.invalidate('ui.render') } })] : []),
1196 ] }))
1197 // the turn's summary: what was done ✓ and what was not ✗, one plain line each
1198 if (sm) for (const [k, mark, list] of [['anjam_shod', '✓', sm.anjam_shod], ['anjam_nashod', '✗', sm.anjam_nashod]]) {
1199 if (list.length) goruh.do_jomle.push(Text({ key: 'hads-sum-' + k, children: [mark + ' ' + list.join(' · ')] }))
1200 }