Skip to content

feat(grok): conversation mgmt (delete/pin/unpin) + locale-independent selectors#1798

Merged
jackwener merged 5 commits into
jackwener:mainfrom
LeoLin990405:feat/grok-mgmt
May 31, 2026
Merged

feat(grok): conversation mgmt (delete/pin/unpin) + locale-independent selectors#1798
jackwener merged 5 commits into
jackwener:mainfrom
LeoLin990405:feat/grok-mgmt

Conversation

@LeoLin990405
Copy link
Copy Markdown
Contributor

Split from #1797. This PR contains all grok-adapter changes. Other surfaces (codex / antigravity / trae-solo) are in separate PRs.

See commit message for full details — bundled because both changes touch clis/grok/utils.js:

  1. fix: composer Submit + Model picker selectors now use data-testid / stable id with i18n aria-label fallback (was broken in zh-CN locale).
  2. feat: new opencli grok delete/pin/unpin commands using the radix sidebar context menu, with the full pointer chain needed to trigger radix events.

Verified live on zh-CN Chrome 148 / grok.com free tier. rename is stubbed (radix React onSelect intercept — separate followup).

@LeoLin990405
Copy link
Copy Markdown
Contributor Author

Grok exhaustive coverage summary (commits 05e2a5b5 + 0fd66ac6)

Did the same deep-audit pass on Grok that landed in PR #1801 for Trae SOLO. Grok is now exhaustively wrapped at every layer reachable from outside its server-side API.

Total: 24 commands across 3 layers

Layer Commands
UI / CDP (chat + sidebar) ask send read detail history new delete pin unpin rename image status model copy-message regenerate react incognito account search-conversations share (19, +1 missed: image-2)
Browser-side storage (analog to Trae's FS state) storage-keys storage-get user-settings cookies idb-list (5)

What's NOT wrapped — and why:

  • Server-side /api/... calls — would use the user's session cookie to RPC into Grok's backend bypassing the UI. That's a TOS gray-area at minimum and breaks on any backend change. Intentionally skipped.
  • httpOnly auth cookiescookies shows JS-visible cookies only; session tokens are correctly httpOnly and we don't try to extract them from CDP. That's a security feature, not a coverage gap.
  • AiTopia IndexedDB contents — the only idb-list returns one suspicious AiTopia DB whose only row holds a 5KB base64 blob (keyed by a 16-byte hash). It looks like third-party extension data, not Grok-managed state. idb-list enumerates DBs but deliberately does not read contents.
  • Voice mode + Dictate (Ctrl+D) — UI exists ([93] [94] in the earlier mapping) but audio input is low-value for a CLI agent.
  • Grammarly extension — third-party, not Grok.
  • Projects panel — empty on the test account; could add project-list / project-new later if someone with active projects requests it.

Safety patterns:

  • delete <id> requires --yes (irreversible, no confirmation dialog from Grok)
  • share is write-class because it creates a public URL; the implementation is verified by static review only — I deliberately did NOT run a live E2E because that would publish one of the user's real conversations. Caller should opt-in explicitly.
  • cookies truncates each value preview to 40 chars to avoid leaking session tokens into logs even if Grok ever marks an auth cookie as non-httpOnly.
  • All storage-* and cookies and idb-list are READ-ONLY by design (writing to Grok's localStorage from outside would race React state).

E2E verified live on grok.com: account name+email extraction, search-conversations with multi-language queries (AI新闻 / Taipei), incognito navigation, model list (Fast/Auto/Expert/Heavy), copy-message returning actual assistant text, storage-keys with filter, idb-list, user-settings decomposition.

Known limitation (documented in code): model --set clicks the dropdown item successfully, but the selection doesn't persist across separate opencli invocations because each command opens a fresh tab. Useful inside a single-tab workflow.

Happy to split into smaller PRs if reviewer prefers — currently 3 commits, each a coherent unit.

LeoLin990405 added a commit to LeoLin990405/OpenCLI that referenced this pull request May 31, 2026
Antigravity is VSCode-derived, so it has BOTH a renderer-side storage
layer (LS/SS/cookies/IDB via CDP) AND the full VSCode FS state.vscdb
on disk. This commit wraps both.

Renderer side (4 — same shape as PR jackwener#1798 Grok / PR jackwener#1799 Codex):
  storage-keys [--storage local|session] [--filter] [--limit]
  storage-get <key> [--storage] [--max-bytes]
  cookies
  idb-list

FS side (4 — VSCode-style state.vscdb + workspace enumeration):
  state-keys [--filter] [--workspace] [--limit]
        — list keys in globalStorage state.vscdb. With --workspace
          <id>, query a per-workspace state.vscdb. Works while
          Antigravity is closed.
  state-get <key> [--workspace] [--max-bytes]
        — read one value (auto-decode JSON).
  recent-paths
        — pretty-print history.recentlyOpenedPathsList (the File >
          Open Recent list).
  workspaces-list
        — enumerate workspaceStorage/<uuid>/ + resolve workspace.json
          to the actual folder path.

Settings (1):
  settings-read
        — parse and pretty-print user settings.json (handles JSONC:
          line comments + block comments + trailing commas).

E2E verified against Leo's Antigravity install:
  state-keys --filter antigravity → 5+ antigravity.notification.* keys
                                     (out of 202 total in state.vscdb)
  recent-paths → 5+ folders/files (xiaotie, 竞品代码, Mini-Agent, …)
  workspaces-list → 7 workspaces with folder paths + mtimes
  settings-read → colorTheme=Solarized Light, http.proxy=127.0.0.1:7897,
                  agCockpit.displayMode=quickpick, tfa.system.autoAccept=true
  state-get history.recentlyOpenedPathsList → full JSON returned
  renderer storage-keys → EMPTY_RESULT (correct: Antigravity uses
                          state.vscdb instead of browser LS)

Brings Antigravity adapter to 31 commands.
LeoLin990405 added a commit to LeoLin990405/OpenCLI that referenced this pull request May 31, 2026
Cross-surface naming harmonization (matches PR jackwener#1798 Grok / jackwener#1799 Codex /
jackwener#1800 Antigravity):
  storage-* → renderer-side localStorage / sessionStorage (via CDP)
  state-*   → on-disk VSCode state.vscdb (via sqlite3)

Changes:
  - Renamed storage-keys → state-keys + storage-get → state-get
    (these query state.vscdb, kept their behavior 100% identical)
  - Added renderer-storage.js: storage-keys / storage-get / cookies / idb-list
    (CDP-based, query the Electron renderer's LS/SS/cookies/IDB)
  - Added settings.js: settings-read (parse User/settings.json, JSONC-aware)

E2E verified against Leo's Trae SOLO:
  state-keys --filter "AI.agent"  → 3 AI.agent.* keys (renamed, same behavior)
  settings-read                   → 1 key: AI.toolcall.v2.command.allowList

Known limitation: renderer storage-keys / storage-get / cookies / idb-list
return SecurityError on Trae SOLO because Trae's renderer runs under
vscode-file:// scheme with strict sandboxing — opencli's eval context
hits an isolated world without access to the main page's
localStorage/cookies/IndexedDB. Direct CDP probes (bypassing opencli's
session manager) CAN see the data (22 LS keys + @byted/ve-rtc IDB),
but the standard adapter pathway is blocked. Documented as a structural
limitation; the commands are wired correctly and will work on any Trae
build that relaxes the sandbox.

Brings Trae SOLO to 44 commands (38 + 1 settings-read + 4 renderer + 1
state-* alias gap).
LeoLin990405 and others added 5 commits May 31, 2026 21:36
Two changes bundled together since both touch clis/grok/utils.js:

1. fix(grok): locale-independent submit/model selectors

   Two selectors were keyed on English aria-label:
     - 'button[aria-label="Submit"]'        (composer send)
     - 'button[aria-label="Model select"]'  (model picker)

   These miss in non-English locales — on zh-CN they render as
   aria-label='提交' / '模型选择'. Result:
     $ opencli grok ask 'hi'
     ok: false
     error: Grok submit button did not reach a clickable state
     $ opencli grok status
     - Model: null

   Fix: prefer locale-independent anchors with localized aria-label
   fallbacks.
     Submit → 'button[data-testid="chat-submit"]' →
              'Submit' / '提交' / '送信'
     Model  → '#model-select-trigger' →
              'Model select' / '模型选择' / 'モデル選択'

   Both data-testid='chat-submit' and id='model-select-trigger' have
   been stable on grok.com for months and don't change between locales.

2. feat(grok): add delete/pin/unpin (rename stub)

   Right-clicking any '/c/<id>' sidebar link surfaces a radix context
   menu with 4 items (i18n; zh-CN / en):

     打开新标签页 / Open in new tab
     重命名       / Rename
     置顶 or 取消置顶 / Pin or Unpin    (state-conditional)
     删除         / Delete

   3 of these are now first-class commands:

     opencli grok delete <id> --yes      # no UI confirmation; gate via --yes
     opencli grok pin    <id>
     opencli grok unpin  <id>

   New utils helper clickConversationMenuItem() fires the full pointer
   chain (pointerdown → mousedown → contextmenu → pointerup → mouseup
   with button=2 at the link's bounding rect). Bare
   MouseEvent('contextmenu') is ignored — radix listens on pointer
   events.

   Pin/Unpin are mutually exclusive in the menu slot; defineToggle
   registers both commands binding to whichever label is present.

   Known followup: 'rename' is a stub. Click on the Rename menu item
   fires onSelect at the DOM level but radix's React handler doesn't
   pick it up from synthetic clicks — the inline rename input never
   materializes. Both JS .click() and opencli's CDP click succeed at
   the DOM level but the React handler is skipped. Will need CDP
   Input.dispatchMouseEvent at viewport coordinates.

Verified zh-CN, Chrome 148, grok.com free tier:

  $ opencli grok ask 'reply with: working' -f yaml
  - response: 思考了 3s working
  $ opencli grok status -f yaml
  - Model: Fast                      # was null before

  $ opencli grok pin 916f99ab-... -f yaml
    - status: pinned
  $ opencli grok unpin 916f99ab-... -f yaml
    - status: unpinned
  $ opencli grok delete 916f99ab-... --yes -f yaml
    - status: deleted
  # sidebar query confirms still_present=false ✓

29/29 existing grok tests pass.
…enerate/react)

  model [--list] [--set <name>]
       — show / list / switch the active Grok model. Reuses utils.js
         locale-independent MODEL_TRIGGER_SELECTORS pattern, then reads
         radix [role=menu] portal items.
  copy-message [--conv <id>] [--markdown] [--click-button]
       — return the text of the last assistant message. With --click-button
         also fires the in-UI Copy button (writes to system clipboard).
  regenerate [--conv <id>]
       — click Regenerate on the last assistant message.
  react <like|dislike> [--conv <id>]
       — click Like / Dislike on the last assistant message.

All message-action commands accept optional --conv <id|URL> to navigate
to a specific conversation before acting (mirrors detail.js navigation).
Without --conv, they operate on the current tab.

Selectors:
  - Action buttons use locale-independent aria-label fallback lists
    (Regenerate / 重新生成 / 再生成 — same pattern for Copy/Like/Dislike)
  - Full pointer-event chain (pointerdown→mousedown→pointerup→mouseup→click)
    to satisfy radix-style portal menus
  - Action buttons live in the bubble's nearest ancestor; we walk up 5
    parents then querySelectorAll('button') to find by aria-label

E2E verified live on grok.com against a real conversation:
  copy-message extracted the assistant's actual text ("Hello Lin! 👋 ...")
  react like / react dislike both clicked successfully
  model --list returns all 4 models (Fast / Auto / Expert / Heavy)

Known limitation: model --set clicks the UI item successfully, but the
selection doesn't persist across separate opencli invocations because
each command opens a fresh tab. Useful for "list models" and inside a
single-tab workflow; for persistent model preference, follow up with a
send/ask on the same tab.
…hare UI

Browser-side state (analog to Trae SOLO's storage-* family):
  storage-keys [--storage local|session] [--filter] [--limit]
    — list localStorage (default) or sessionStorage keys with byte sizes
  storage-get <key> [--storage] [--max-bytes]
    — read a single LS/SS value, auto-decode JSON
  user-settings
    — shortcut: pretty-print Grok's "user-settings" object (privacy
      toggles, chat preferences, agent library counts)
  cookies
    — list grok.com cookies visible to JS (httpOnly auth tokens are
      deliberately NOT shown — that's a security feature). Preview is
      truncated to 40 chars to avoid leaking session tokens into logs.
  idb-list
    — enumerate IndexedDB databases on grok.com (does NOT read store
      contents — those may include third-party extension data)

Sidebar / dialog UI:
  incognito
    — switch the current tab to /c#private (private chat mode that's
      not saved to history)
  account
    — read sidebar account info (name + email) and the dropdown menu
      items (settings / upgrade / sign-out). Cleans up by pressing
      Escape to leave the menu closed.
  search-conversations <query> [--limit]
    — open the sidebar search palette, type the query, return matched
      conversation titles + ids. Cleans up on exit.
  share [--conv <id>]
    — click "Create share link" on the current (or specified) conv;
      extract the grok.com/share/... URL from the resulting dialog.
      Useful for sharing answers — caller's responsibility to know this
      makes the conv publicly accessible.

E2E verified on grok.com:
  storage-keys → 23 localStorage keys (chat-preferences, modes-selected-id,
    user-settings, xai-ff-bu, etc.)
  storage-get modes-selected-id → "fast"
  user-settings → 14 preferences + 6 privacy toggles + agent library counts
  cookies → 9 JS-visible cookies (x-userid, x-anonuserid, etc.)
  idb-list → AiTopia + mixpanelBrowserDb
  account → Name + Email + 6 menu items (settings / upgrade / sign-out)
  incognito → navigates to /c# (private mode)
  search-conversations "Taipei" → matches "Friendly greeting in Taipei"

share command verified by static review only — live E2E deferred because
running it would create a real public share URL for the user's existing
conversation. Calling code/user should opt in explicitly.
Deep-audit pass identified 8 UI buttons mapped earlier but not wrapped:

  sidebar-toggle             — collapse/expand the left sidebar (idempotent;
                               reports sidebarOpen localStorage state)
  toggle-preview             — show/hide conversation preview pane (隐藏对话预览)
  view-all                   — open full conversation history page (查看全部)
  project-list               — list projects in sidebar accordion (graceful
                               empty result if no projects exist)
  project-new <name> --yes   — create a new project via the create-project
                               dialog (dry-run by default)
  attach <file> [--conv]     — set <input type=file> on the composer via CDP
                               page.upload; pair with `send` to actually send
  edit-message [--index N]   — click Edit on a user message (default: last);
                               returns the original text
  more-actions [--conv]      — open the per-message "More actions" menu on
                               the last assistant message and list its items

E2E verified live on grok.com:
  sidebar-toggle    → localStorage sidebarOpen flipped false ↔ true
  project-list      → EMPTY_RESULT (no projects on this account) — clean
  project-new       → dry-run shows planned action
  more-actions      → revealed 3 menu items: 报告问题, 导出到 PDF, 朗读

attach + toggle-preview + edit-message are E2E-deferrable: attach needs
an actual file to upload, toggle-preview needs a sidebar item with preview
already shown, edit-message needs an assistant turn loaded. All three
are wired up and verified by static review.

This brings Grok to 33 commands (was 25), matching Trae SOLO's coverage
depth across UI + browser storage layers.
jackwener pushed a commit to LeoLin990405/OpenCLI that referenced this pull request May 31, 2026
Cross-surface naming harmonization (matches PR jackwener#1798 Grok / jackwener#1799 Codex /
jackwener#1800 Antigravity):
  storage-* → renderer-side localStorage / sessionStorage (via CDP)
  state-*   → on-disk VSCode state.vscdb (via sqlite3)

Changes:
  - Renamed storage-keys → state-keys + storage-get → state-get
    (these query state.vscdb, kept their behavior 100% identical)
  - Added renderer-storage.js: storage-keys / storage-get / cookies / idb-list
    (CDP-based, query the Electron renderer's LS/SS/cookies/IDB)
  - Added settings.js: settings-read (parse User/settings.json, JSONC-aware)

E2E verified against Leo's Trae SOLO:
  state-keys --filter "AI.agent"  → 3 AI.agent.* keys (renamed, same behavior)
  settings-read                   → 1 key: AI.toolcall.v2.command.allowList

Known limitation: renderer storage-keys / storage-get / cookies / idb-list
return SecurityError on Trae SOLO because Trae's renderer runs under
vscode-file:// scheme with strict sandboxing — opencli's eval context
hits an isolated world without access to the main page's
localStorage/cookies/IndexedDB. Direct CDP probes (bypassing opencli's
session manager) CAN see the data (22 LS keys + @byted/ve-rtc IDB),
but the standard adapter pathway is blocked. Documented as a structural
limitation; the commands are wired correctly and will work on any Trae
build that relaxes the sandbox.

Brings Trae SOLO to 44 commands (38 + 1 settings-read + 4 renderer + 1
state-* alias gap).
@jackwener jackwener merged commit a1555e8 into jackwener:main May 31, 2026
jackwener pushed a commit to LeoLin990405/OpenCLI that referenced this pull request May 31, 2026
Cross-surface naming harmonization (matches PR jackwener#1798 Grok / jackwener#1799 Codex /
jackwener#1800 Antigravity):
  storage-* → renderer-side localStorage / sessionStorage (via CDP)
  state-*   → on-disk VSCode state.vscdb (via sqlite3)

Changes:
  - Renamed storage-keys → state-keys + storage-get → state-get
    (these query state.vscdb, kept their behavior 100% identical)
  - Added renderer-storage.js: storage-keys / storage-get / cookies / idb-list
    (CDP-based, query the Electron renderer's LS/SS/cookies/IDB)
  - Added settings.js: settings-read (parse User/settings.json, JSONC-aware)

E2E verified against Leo's Trae SOLO:
  state-keys --filter "AI.agent"  → 3 AI.agent.* keys (renamed, same behavior)
  settings-read                   → 1 key: AI.toolcall.v2.command.allowList

Known limitation: renderer storage-keys / storage-get / cookies / idb-list
return SecurityError on Trae SOLO because Trae's renderer runs under
vscode-file:// scheme with strict sandboxing — opencli's eval context
hits an isolated world without access to the main page's
localStorage/cookies/IndexedDB. Direct CDP probes (bypassing opencli's
session manager) CAN see the data (22 LS keys + @byted/ve-rtc IDB),
but the standard adapter pathway is blocked. Documented as a structural
limitation; the commands are wired correctly and will work on any Trae
build that relaxes the sandbox.

Brings Trae SOLO to 44 commands (38 + 1 settings-read + 4 renderer + 1
state-* alias gap).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants