SLOPSHOPPER

hads

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

newbandguardcommandpromptmodel
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · hads
› fix the failing auth test and add an audit log call ⏺ Read(src/auth.ts) ⎿ Read 6 lines ⏺ Update(src/auth.ts) ⎿ Added 2 lines, removed 1 line ⏺ Bash(bun test) ⎿ 3 pass, 1 fail ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM › /hads ⎿ hads: حدس در این چت خاموش است؛ اول /hads on ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts
README

Claude Code Extension RTL Fix

A simple script to add RTL (Right-to-Left) text support to the Claude Code extension in VS Code-based IDEs.

Supported IDEs

  • VSCode
  • VSCode Insiders
  • Cursor
  • Windsurf
  • Windsurf Next
  • Devin

Supported RTL Languages

  • Persian (Farsi)
  • Arabic
  • Urdu
  • Pashto
  • Kurdish
  • Dari
  • Sindhi
  • And other RTL scripts

Installation

macOS / Linux

# 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
Options
# 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
VariableMeaningDefault
CHAT_FONT_SIZEconversation text15px
CHAT_LINE_HEIGHTconversation line height1.8
CODE_FONT_SIZEcode blocks and tables12px
USER_MSG_LINESlines of your own messages (0 = full)1
SIDE_BTN_FONTquick-command buttons9px
CHROME_FONT_SIZEheader, footer, labels10px

Windows

# 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
Options
# 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

After Claude Code Updates

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

Chat does not jump to the bottom when you send

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.

The in-panel settings (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.

ControlWhat 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
selpush the editor's current selection into the chat
شمارندهcharacter count of the composer, plus the app's own usage line when it renders one
savcopy the current settings as JSON for ~/.claude-rtl-sizes.json
rstback to defaults

A folded Thinking or tool-call block opens on hover, and a click pins it open.

Quoting a message

Select any text in the conversation and right-click it:

Menu entryWhat 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

How It Works

The script patches the Claude Code extension webview with RTL-specific CSS:

  • Sets direction: rtl for text elements
  • Preserves LTR for code blocks and diffs
  • Optionally adds Vazirmatn font for better Persian/Arabic rendering

The 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.

The composer is two layers — do not style it

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.

The launchd agent uses --with-font

com.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.

Troubleshooting

No Claude Code extensions found

  • Make sure Claude Code extension is installed in your IDE
  • Extension paths:
  • macOS/Linux: ~/.<ide-name>/extensions/anthropic.claude-code-*/webview/
  • Windows: %USERPROFILE%\.<ide-name>\extensions\anthropic.claude-code-*\webview\

Changes not visible

  • Press Cmd+R (Reload Window). The inline injection survives the webview cache, so a reload is enough — a full restart is not needed.
  • If it still looks stale, clear the webview cache: rm -rf ~/Library/Application\ Support/Code/Cache/* ~/Library/Application\ Support/Code/Code\ Cache/*

The patch disappeared after an extension update

  • An update installs a new extension folder, which starts unpatched. Run the script again; it patches every installed version and prints a [WARN] line for any version it finds unpatched.

PowerShell execution policy error (Windows)

  • Run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
  • Or run the script with: powershell -ExecutionPolicy Bypass -File .\fix-rtl-claude.ps1

Known Issues

High CPU Usage

This 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.

License

MIT License - See LICENSE file for details.

Contributing

Contributions are welcome! Feel free to open issues or submit pull requests.

ChatGPT / Codex extension in VS Code

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.

Compact ChatGPT layout

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.

Source 1 files
hooks/register.js 1318 lines
1// حدس: «دو جمله» 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    }