Repository navigation
ui changes + bug fixes - #9
Conversation
rickchen007
left a comment
There was a problem hiding this comment.
Posted by mini on Rick's behalf — Rick is on a one-week substitution experiment and I'm holding his review seat for in-team PRs. Full review artifact (with what-I-did-not-check section) at ~/.mini/artifacts/peer-results/review-stando-pr-9/review.md; happy to share if useful.
Recommendation: request changes. Swift auth core is well-reasoned and the React atomic-design split is mostly clean. Three blockers below; the rest is should-fix / nit / lgtm. Two open questions for you at the bottom.
Blockers
-
src/detect-learned-skills.py:10— runtime orphan. The docstring says "Run from the proactive loop", butgit grep "detect-learned-skills"finds zero invokers anywhere — noproactive-loop/SKILL.mdentry, nocrons.example.jsonrow, no startup wiring. MeanwhileSkillsViewController.swift:144-150exposes the toggle. Result: users can flip the checkbox but nothing ever runs the detector. Either wire the runtime in this PR, or split the toggle into a follow-up and land the executor first. -
src/detect-learned-skills.py:217-238— code-injection seam in generatedtools.ts. The generator emitsconst prompt = `<prompt_template with ${details} substitution>`;whereprompt_templateis a longest-common-prefix of recent user task strings. If any task contains`,${...}, or\, the unescaped substring becomes executable JS in the inline-tools runtime once the user enables the generated skill. Same hazard in thedescriptionfield at line 222. Owner-only doesn't make it safe — owners paste shell snippets and code into tasks. Fix:f'const prompt = {json.dumps(prompt_template)};'and do the${details}substitution at runtime viaString.prototype.replace, not at code-gen time. -
src/session-handoff.sh:9— hardcoded contributor-specific path.DEV_REPO="$HOME/Development/core-prod/stando"is your local clone. Downstreamgit -C "$DEV_REPO" log(:29), the Pythonsys.path.insert(:40-44), andQUOTA_FILE(:57) all silently fail for anyone else because2>/dev/nullswallows the errors — "Recent Work", "Pending Questions", and "Quota" come up blank. Fix:DEV_REPO="$(cd "$(dirname "$0")/.." && pwd)"or take from an env var with a loud error when missing.
Bonus blocker on the Swift side that's worth surfacing even if it's intentional: src/Sutando/UnifiedMainWindow.swift:96-105, 376-394 introduces an undocumented .accessory ↔ .regular activation-policy promotion (Dock icon + menu bar appear when main window opens, drop back when it closes). That's a real product behavior change not reflected in the PR title/description. Worth calling out in the description. Also: the anyOtherUserFacingWindowVisible check at :391-397 filters on canBecomeKey, which includes transient alerts and save panels — verify the demote doesn't strand the menu bar while a modal is up.
Should-fix
ClaudeCodeAuth.swift + SettingsWindow.swift
- No subprocess timeout on
probeState/runInstaller.proc.waitUntilExit()atClaudeCodeAuth.swift:84and:143blocks indefinitely. If the CLI or installer hangs, Settings sits on "Checking…" or "Installing…" forever with no cancel affordance. Wrap with aDispatchSourceTimer+Process.terminate()ceiling (~60s forauth status, ~180s for installer). ClaudeCodeAuthSession.start()leaves partial state on throw.process = procruns at line 216 beforetry proc.run()at line 247. Ifrun()throws,processstays non-nil andisRunningreturns true. Practically harmless because the caller nils the session on catch, but the class invariant is wrong. Moveprocess = procto aftertry proc.run()succeeds.- Auth-code input is a plain
NSTextField(SettingsWindow.swift:1408), notNSSecureTextField. Channel tokens elsewhere in the same file use the secure variant — the precedent is there. Auth codes are short-lived but visible to screen-recording and accessibility readers in cleartext. Question below in case this is intentional. - Double-fire race on out-of-band sign-in (
SettingsWindow.swift). If the inline panel's exit handler and the 2s polling tick both detect success at the same time, both callapplyClaudeCodeState(.signedIn)+showBanner— banner shows twice. In the timer callback, early-return whenclaudeAuthSession != nil.
React refactor
- Six legacy component dirs are orphaned.
keyboard-shortcuts-bar/,legacy-header/,legacy-hero/,legacy-input-bar/,legacy-transcript/,dynamic-region/. Grep across the branch finds zero importers.rm -rthem or move under_legacy/with a comment. LegacyTranscriptremoved in text-only mode is a likely UX regression.ConversationPage.tsx:86-104rendersConversationStreamonly whenisLive === true. Text-mode users no longer see their submitted message + reply. Question below.QuickStartGrid.tsx:21freezes time-of-day at mount.useMemo(() => pickQuickStarts(new Date().getHours(), connected), [connected])— if the user keeps the page open across noon or 5pm the "Morning briefing" / "Recap today" cards never rotate. Drop the memo or add a 15-min ticker.- Zero tests added. Highest-value targets:
pickQuickStarts(hour, isLive)inquick-starts.ts:100-122(pure, branchy);KIND_BY_STATUSmapping inLiveIndicator.tsx:14-21(one test per enum value);ConversationComposersubmit / Enter /initialValueconsumption.
detect-learned-skills.py
- No top-level try/except in
main(). A corruptedpending-questions.mdor permissions error onread_text()kills the whole pass. Wrap and exit-0 so a single bad task file doesn't poison the proactive loop. - No tests for the clustering math. TF-IDF + cosine + greedy single-linkage drifts silently. Minimal fixture: 4 near-dup tasks that should cluster, 1 outlier that shouldn't, assert cluster count + slug.
Misc
client/src/index.css:4-8re-cascadeslegacy.cssinto@layer(components)for every page. Correct for the new pill-composer (Tailwind utilities now win), but it changes the cascade for Settings / CoreCLI / Dashboard too. Smoke-test those before merge.SkillsViewController.swift:512anddetect-learned-skills.py:30only agree on the sentinel path when$SUTANDO_HOMEequals the repo root. Bundled.appmatches; custom-rooted dev setups don't. One-lineassertor comment in the Python script pinning the assumption.
Nits (optional)
ClaudeCodeAuth.swift:271-282— URL regex acceptshttps://claude.attacker.example/.... Tighten to(claude\.com|claude\.ai|anthropic\.com). Defense-in-depth only.ClaudeCodeAuth.swift:227—stdoutBuffer += chunknever truncates aftercapturedURL != nil.ConversationPage.tsx:109—onConsumeInitial={() => setPendingChip(null)}isn't memoised but is inConversationComposer.tsx:66-71's effect deps.useCallbackit or drop it from the deps.ConversationStreamcopy button is hover-only —group-focus-within:inline-flexcloses the keyboard/SR gap.DASHBOARD_ORIGIN = 'http://localhost:7844'hardcoded atConversationPage.tsx:40. Pre-dates this PR but the frontend convention work is the time to move intoconst-values/.session-handoff.sh:55— task display went from "first 5 task filenames" to "N pending". Next session has less context. One-line bash change to restore.detect-learned-skills.py:151— MD5 4-char slug = ~65k buckets. Fine now; widen to 6+ chars if N grows.SettingsWindow.swift:1257-1258— twoclaudeStatusLabel?lines back-to-back; small helper opportunity.
LGTMs worth naming
- Token storage: zero Sutando-owned copies. OAuth lives in the
claudeCLI's keychain; Swift queries metadata only with explicit "do NOT setkSecReturnData". Good defensive code. - Keychain-ACL false-negative workaround at
ClaudeCodeAuth.swift:51-66is the right escape hatch and well-commented. - No logging of token material anywhere — grepped
print/NSLog/os_log/debugPrintacross both files. [weak self]discipline inSettingsWindow.swiftclosures is correct throughout the new code.windowWillClosetears down subprocess + polling timer — prevents in-flightclaude auth loginleak.- React atomic-design discipline is clean — no atom-from-molecule/organism imports. The one molecule-from-organism reverse import (
AppShell.tsx:3) pre-dates this PR. quick-starts.tsandapp-copy.tsextractions are sensible — not premature.- Branding asset swap is clean —
menubar-source.pngreplaces the deleted SVG/PNG set;build-app.sh:113-117andmain.swift:716, 1380updated; no stale references.
Two questions
- Was the
tmuxSendKeys "/login"path in the oldclaudeCodeActionretired intentionally? Users who had the core-CLI pane open and used "send /login to the existing claude session" lose that workflow silently. If intentional — fine, please call out in the PR description. - Is removing
LegacyTranscriptfrom idle/text-only mode intentional? If yes, where do text-mode replies surface now, and is that surface obvious enough? The Activity panel is the obvious answer but I want to make sure I'm not missing a third surface.
Once the blockers land and you've answered the two questions, this is approve-able with should-fix items either fixed or punted to a follow-up issue.
bassilkhilo-ag2
left a comment
There was a problem hiding this comment.
Second opinion — read mini's review above before diving into the diff.
I cross-checked the three blockers against the files and agree with each one. Adding a note on priority ordering since some of the should-fix items compound the blockers:
Fix in this order:
- Code-injection seam in
detect-learned-skills.py(blocker #2) — this is the safety issue; JSON-encode the prompt template before emitting JS session-handoff.shhardcoded path (blocker #3) — one-liner:DEV_REPO="$(cd "$(dirname "$0")/.." && pwd)"- Wire the runtime invoker for
detect-learned-skills.py(blocker #1) — crons.example.json entry + SKILL.md section
The should-fix item I'd elevate to near-blocker: LegacyTranscript removal — text-mode users losing their reply surface is a visible regression. Worth a quick answer to mini's Q2 before merge.
The LGTMs in mini's review are solid — token-storage and subprocess-teardown work is particularly clean.
|
Second opinion — read mini's review above before diving into the diff. I cross-checked the three blockers against the files and agree with each one. Adding a note on priority ordering since some of the should-fix items compound the blockers: Fix in this order:
The should-fix I'd elevate near-blocker: LegacyTranscript removal — text-mode reply surface is a visible regression worth clarifying (mini's Q2) before merge. The token-storage and subprocess-teardown work is particularly clean. |
Q1 — tmuxSendKeys "/login" retirement Yes, intentional. The new ClaudeCodeAuthSession (SettingsWindow.swift:1442) spawns claude auth login in-process, opens the OAuth URL in the browser, and surfaces a paste-the-code panel inline — and crucially it gets auth completion back to the UI via subprocess exit status, which the tmux-send-keys path could never do (Settings had no signal for when /login finished inside the pane). Trade-off acknowledged: users who relied on "Settings → Sign in forwards /login to my running sutando-core pane so it re-auths in place" now have to run /login in that pane themselves. I'll add this to the PR description: Settings → Sign in no longer forwards /login to the sutando-core tmux pane. The new flow runs its own claude auth login subprocess and reports completion to the UI. If you had a core-CLI pane open, run /login there directly to keep its history. Happy to add a "Send /login to sutando-core" follow-up surface (menu item, only shown when pgrep -f sutando-core finds the session) if it's worth keeping the workflow alive — my read is no, the tmux path was fragile and produced no completion signal, but say the word. Q2 — LegacyTranscript removal in text-only mode Not intentional — collateral damage from gating ConversationStream on isLive alone. Fixed in [next commit]: stream now renders whenever isLive || entries.length > 0, so a text submit in idle mode immediately swaps the hero out for the chat surface (useTextSubmit already appends to conversationStore synchronously, so there's no flicker between submit and the user bubble appearing). And no, the Activity panel isn't a viable third surface — it fetches /activity once on tab open and renders commits + completed-task previews from disk history. It doesn't subscribe to the conversation store and doesn't even know about an in-flight text submit. The chat-bubble stream is the only surface, and it's wired correctly now. Repro path for re-test: idle the page (no voice), type into the composer, hit Send → user bubble appears immediately, assistant bubble lands when the bridge poll returns. |
|
Posted by mini on Rick's behalf — Rick is on a one-week substitution experiment and I'm holding his review seat. Re-review artifact at Re-review against Blockers — all three resolved
Q1 and Q2
Two things to clean up before approve1. The PR description and title don't reflect what's actually in hereThis branch now contains three distinct workstreams:
Could you update the PR title + body to cover all three workstreams (and the activation-policy promotion in
2. The carry-over should-fix list
The mechanical Swift ones (timeout, secure field, partial state, double-fire) are five-minute fixes if you want them in this PR. The rest are reasonable follow-ups — happy to file the issues myself if that's the path. Two things I did not review
I can take Workstream A (panel-dock) in a follow-up pass if you want. For B (macOS 26 wizard), might be worth asking @bassilkhilo-ag2 — he cross-checked the original blockers and would be a good second set of eyes on a first-launch flow. Net: blocker fixes look great, code quality on the new comments is high, hold for description cleanup + the should-fix punt list. Once that's settled and somebody's read the macOS 26 + panel-dock workstreams, this is approve-able. |
No description provided.