muonroi-cli 1.8.5 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- package/dist/src/mcp/ee-tools.js +0 -194
|
@@ -6,11 +6,13 @@ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
|
|
|
6
6
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { clearLastSurfacedMatches, getDefaultEEClient, getLastSurfacedMatches } from "../ee/intercept.js";
|
|
8
8
|
import { deliberateCompact } from "../flow/compaction/index.js";
|
|
9
|
+
import { stageProgress } from "../flow/compaction/progress.js";
|
|
9
10
|
import { writeScaffoldCheckpoint } from "../flow/scaffold-checkpoint.js";
|
|
10
11
|
import { appendCrashLog, setActiveEeYield } from "../index.js";
|
|
11
12
|
import { POPULAR_MCP_CATALOG } from "../mcp/catalog.js";
|
|
12
13
|
import { parseEnvLines, parseHeaderLines } from "../mcp/parse-headers.js";
|
|
13
14
|
import { toMcpServerId, validateMcpServerConfig } from "../mcp/validate.js";
|
|
15
|
+
import { ASK_USER_DISMISSED, buildAskUserQuestion } from "../orchestrator/ask-user.js";
|
|
14
16
|
import { createCompactionSummaryMessage } from "../orchestrator/compaction.js";
|
|
15
17
|
import { Agent } from "../orchestrator/orchestrator.js";
|
|
16
18
|
import { buildCompactResumeMessage, detectProactiveCompactRequest, } from "../orchestrator/proactive-compact-detector.js";
|
|
@@ -39,9 +41,12 @@ import { initialCardState, reduceCardKey, } from "./components/council-question-
|
|
|
39
41
|
import { cycleRoundSelection } from "./components/council-rail-rounds.js";
|
|
40
42
|
import { reapStatuses, upsertStatus } from "./components/council-status-list.js";
|
|
41
43
|
import { BB_TEMPLATE_OPTIONS, FE_STACK_OPTIONS, initialInitNewFormState, } from "./components/init-new-form-card.js";
|
|
44
|
+
import { findLastCollapsibleIndex } from "./components/message-view.js";
|
|
45
|
+
import { groupToolEntries } from "./utils/group-tool-entries.js";
|
|
42
46
|
import { initialPointToExistingFormState, } from "./components/point-to-existing-form-card.js";
|
|
43
47
|
import { useRolePalette } from "./components/role-palette.js";
|
|
44
48
|
import { usePairQuoteBuffer } from "./components/use-pair-quote-buffer.js";
|
|
49
|
+
import { mapCouncilStatusToSpeakerEvent } from "./council-harness-event.js";
|
|
45
50
|
import { useAgentEditor } from "./hooks/use-agent-editor.js";
|
|
46
51
|
import { useMcpEditor } from "./hooks/use-mcp-editor.js";
|
|
47
52
|
import { useModelPicker } from "./hooks/use-model-picker.js";
|
|
@@ -599,7 +604,11 @@ export function useAppLogic(props) {
|
|
|
599
604
|
setConfiguredProviders(resolvePickerProviders(SPLASH_PROVIDERS, configured, (p) => getModelsForProvider(p).length > 0));
|
|
600
605
|
setProvidersWithKey(new Set(configured));
|
|
601
606
|
}
|
|
602
|
-
catch {
|
|
607
|
+
catch (err) {
|
|
608
|
+
// This resets every chip to "no key — press K". Silently, that reads as
|
|
609
|
+
// "the key I just set was not saved" when the credentials are in fact on
|
|
610
|
+
// disk and only the lookup failed — so say so instead of swallowing it.
|
|
611
|
+
console.error(`[providers] credential lookup failed; chips will show no key: ${err instanceof Error ? err.message : String(err)}`);
|
|
603
612
|
setConfiguredProviders([...SPLASH_PROVIDERS]);
|
|
604
613
|
setProvidersWithKey(new Set());
|
|
605
614
|
}
|
|
@@ -629,9 +638,19 @@ export function useAppLogic(props) {
|
|
|
629
638
|
try {
|
|
630
639
|
const ok = await setKeyForProvider(apiKeyPrompt.provider, key);
|
|
631
640
|
if (!ok) {
|
|
632
|
-
setApiKeyPrompt({ ...apiKeyPrompt, error: "
|
|
641
|
+
setApiKeyPrompt({ ...apiKeyPrompt, error: "Could not store key. Try `export <PROVIDER>_API_KEY=…`." });
|
|
633
642
|
return;
|
|
634
643
|
}
|
|
644
|
+
// Same reason as the OAuth path: boot skips a provider with no
|
|
645
|
+
// credentials, so without this the key is stored but the provider stays
|
|
646
|
+
// unusable until the next start.
|
|
647
|
+
try {
|
|
648
|
+
const { rewarmProviderFactory } = await import("../providers/warm.js");
|
|
649
|
+
await rewarmProviderFactory(apiKeyPrompt.provider);
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
console.error(`[providers] ${apiKeyPrompt.provider} key stored but its factory could not be rebuilt; a restart may be needed: ${err instanceof Error ? err.message : String(err)}`);
|
|
653
|
+
}
|
|
635
654
|
await refreshProvidersWithKey();
|
|
636
655
|
setApiKeyPrompt(null);
|
|
637
656
|
}
|
|
@@ -639,85 +658,92 @@ export function useAppLogic(props) {
|
|
|
639
658
|
setApiKeyPrompt({ ...apiKeyPrompt, error: e.message });
|
|
640
659
|
}
|
|
641
660
|
}, [apiKeyPrompt, refreshProvidersWithKey]);
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
661
|
+
// ── OAuth subscription login (browser-based) for openai / xai ───────────
|
|
662
|
+
// One auth mode per provider: a successful OAuth login clears any stored API
|
|
663
|
+
// key for that provider (exclusivity). Runs the registry login() flow, which
|
|
664
|
+
// opens the browser and resolves when the loopback callback completes.
|
|
665
|
+
const [oauthProviders, setOAuthProviders] = useState(() => new Set());
|
|
666
|
+
const [oauthLogin, setOAuthLogin] = useState(null);
|
|
667
|
+
const oauthCancelRef = useRef(false);
|
|
668
|
+
// Aborts the in-flight sign-in itself, not just the UI's interest in it —
|
|
669
|
+
// see cancelProviderOAuth.
|
|
670
|
+
const oauthAbortRef = useRef(null);
|
|
671
|
+
// Populate the OAuth-capable provider set once, from the OAuth registry.
|
|
672
|
+
useEffect(() => {
|
|
673
|
+
let alive = true;
|
|
674
|
+
void (async () => {
|
|
675
|
+
try {
|
|
676
|
+
const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
|
|
677
|
+
const ids = await listOAuthProviderIds();
|
|
678
|
+
if (alive)
|
|
679
|
+
setOAuthProviders(new Set(ids));
|
|
680
|
+
}
|
|
681
|
+
catch {
|
|
682
|
+
/* registry unavailable — no OAuth affordance */
|
|
683
|
+
}
|
|
684
|
+
})();
|
|
685
|
+
return () => {
|
|
686
|
+
alive = false;
|
|
687
|
+
};
|
|
688
|
+
}, []);
|
|
689
|
+
const startProviderOAuth = useCallback(async (provider) => {
|
|
690
|
+
oauthCancelRef.current = false;
|
|
691
|
+
// Abort the PREVIOUS attempt's server before starting another: the
|
|
692
|
+
// browser flow binds a loopback callback on a two-port set, so a
|
|
693
|
+
// still-running one would make this attempt fail to bind.
|
|
694
|
+
oauthAbortRef.current?.abort();
|
|
695
|
+
oauthAbortRef.current = new AbortController();
|
|
696
|
+
setOAuthLogin({ provider, error: null });
|
|
652
697
|
try {
|
|
653
|
-
const {
|
|
654
|
-
const
|
|
655
|
-
if (!
|
|
656
|
-
|
|
698
|
+
const { getOAuthProviderConfig } = await import("../providers/auth/registry.js");
|
|
699
|
+
const cfg = await getOAuthProviderConfig(provider);
|
|
700
|
+
if (!cfg) {
|
|
701
|
+
setOAuthLogin({ provider, error: "OAuth is not available for this provider." });
|
|
657
702
|
return;
|
|
658
703
|
}
|
|
659
|
-
const
|
|
660
|
-
|
|
661
|
-
if (!list.ok) {
|
|
662
|
-
setBwSync({ ...bwSync, loading: false, error: list.error });
|
|
704
|
+
const tokens = await cfg.provider.login({ signal: oauthAbortRef.current?.signal });
|
|
705
|
+
if (oauthCancelRef.current)
|
|
663
706
|
return;
|
|
707
|
+
const { saveTokens } = await import("../providers/auth/token-store.js");
|
|
708
|
+
await saveTokens(provider, tokens);
|
|
709
|
+
// Exclusivity: OAuth login clears any stored API key for this provider.
|
|
710
|
+
try {
|
|
711
|
+
const { clearEnvVar } = await import("../providers/env-store.js");
|
|
712
|
+
const { ENV_BY_PROVIDER } = await import("../providers/keychain.js");
|
|
713
|
+
clearEnvVar(ENV_BY_PROVIDER[provider]);
|
|
664
714
|
}
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const providerName = it.name.slice(prefix.length);
|
|
668
|
-
if (SPLASH_PROVIDERS.includes(providerName) && it.notes.length >= 20) {
|
|
669
|
-
matched.push({ provider: providerName, key: it.notes });
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
if (matched.length === 0) {
|
|
673
|
-
setBwSync({
|
|
674
|
-
...bwSync,
|
|
675
|
-
loading: false,
|
|
676
|
-
error: `No items found in vault with prefix '${prefix}<provider>'`,
|
|
677
|
-
});
|
|
678
|
-
return;
|
|
715
|
+
catch (err) {
|
|
716
|
+
console.error(`[providers] could not clear the stored ${provider} key after OAuth login: ${err instanceof Error ? err.message : String(err)}`);
|
|
679
717
|
}
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
items: matched,
|
|
684
|
-
selected: new Set(matched.map((m) => m.provider)),
|
|
685
|
-
focusIndex: 0,
|
|
686
|
-
loading: false,
|
|
687
|
-
error: null,
|
|
688
|
-
});
|
|
689
|
-
}
|
|
690
|
-
catch (e) {
|
|
691
|
-
setBwSync({ ...bwSync, loading: false, error: e.message });
|
|
692
|
-
}
|
|
693
|
-
}, [bwSync]);
|
|
694
|
-
const commitBwImport = useCallback(async () => {
|
|
695
|
-
if (!bwSync || bwSync.phase !== "picker")
|
|
696
|
-
return;
|
|
697
|
-
setBwSync({ ...bwSync, loading: true, error: null });
|
|
698
|
-
let imported = 0;
|
|
699
|
-
let failed = 0;
|
|
700
|
-
for (const item of bwSync.items) {
|
|
701
|
-
if (!bwSync.selected.has(item.provider))
|
|
702
|
-
continue;
|
|
718
|
+
// The factory bakes in the auth it saw when it was built, so the tokens
|
|
719
|
+
// we just saved reach nothing until it is rebuilt — that is why signing
|
|
720
|
+
// in only took effect after restarting the session.
|
|
703
721
|
try {
|
|
704
|
-
const
|
|
705
|
-
|
|
706
|
-
imported++;
|
|
707
|
-
else
|
|
708
|
-
failed++;
|
|
722
|
+
const { rewarmProviderFactory } = await import("../providers/warm.js");
|
|
723
|
+
await rewarmProviderFactory(provider);
|
|
709
724
|
}
|
|
710
|
-
catch {
|
|
711
|
-
|
|
725
|
+
catch (err) {
|
|
726
|
+
console.error(`[providers] ${provider} signed in but its factory could not be rebuilt; a restart may be needed: ${err instanceof Error ? err.message : String(err)}`);
|
|
712
727
|
}
|
|
728
|
+
await refreshProvidersWithKey();
|
|
729
|
+
setOAuthLogin(null);
|
|
713
730
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
|
|
731
|
+
catch (e) {
|
|
732
|
+
if (oauthCancelRef.current)
|
|
733
|
+
return;
|
|
734
|
+
setOAuthLogin({ provider, error: e.message });
|
|
735
|
+
}
|
|
736
|
+
}, [refreshProvidersWithKey]);
|
|
737
|
+
const cancelProviderOAuth = useCallback(() => {
|
|
738
|
+
oauthCancelRef.current = true;
|
|
739
|
+
// Esc used to close the card and leave the sign-in running: its loopback
|
|
740
|
+
// server kept port 1455/1457 for the full 5-minute callback timeout, so the
|
|
741
|
+
// next attempt could not bind and only restarting the CLI recovered it.
|
|
742
|
+
// Abort ends the flow, which closes the server and frees the port now.
|
|
743
|
+
oauthAbortRef.current?.abort();
|
|
744
|
+
oauthAbortRef.current = null;
|
|
745
|
+
setOAuthLogin(null);
|
|
746
|
+
}, []);
|
|
721
747
|
// Sync React model state with status bar store so chat input reflects
|
|
722
748
|
// per-turn router upgrades (brain EE upgrade, warm/cold routing, etc.)
|
|
723
749
|
useEffect(() => {
|
|
@@ -728,7 +754,7 @@ export function useAppLogic(props) {
|
|
|
728
754
|
}, [setModel]);
|
|
729
755
|
const initialHasApiKey = agent.hasApiKey();
|
|
730
756
|
const [hasApiKey, setHasApiKey] = useState(initialHasApiKey);
|
|
731
|
-
const [messages, setMessages] = useState(() => agent.getChatEntries());
|
|
757
|
+
const [messages, setMessages] = useState(() => groupToolEntries(agent.getChatEntries()));
|
|
732
758
|
const [streamContent, setStreamContent] = useState("");
|
|
733
759
|
// Reasoning state: track activity + last-elapsed for a "💭 Thought for Ns"
|
|
734
760
|
// pill instead of dumping CoT into the chat (saves 80–120 setState/sec on
|
|
@@ -808,6 +834,8 @@ export function useAppLogic(props) {
|
|
|
808
834
|
// Peek toggle (ctrl+e) for the todo panel while it auto-collapses during a
|
|
809
835
|
// council debate. Default collapsed; expands back to the full panel on demand.
|
|
810
836
|
const [councilTodoExpanded, setCouncilTodoExpanded] = useState(false);
|
|
837
|
+
// Non-null only while a /compact is in flight; drives CompactProgressCard.
|
|
838
|
+
const [compactRun, setCompactRun] = useState(null);
|
|
811
839
|
const [councilInfoCards, setCouncilInfoCards] = useState([]);
|
|
812
840
|
// P3 — council metadata for the context rail (leader/panel/budget/research/
|
|
813
841
|
// cost), upsert-merged from incremental council_meta patches.
|
|
@@ -824,6 +852,14 @@ export function useAppLogic(props) {
|
|
|
824
852
|
// global (all/live rounds). Mirrored into a ref so the global key handler can
|
|
825
853
|
// read the current round list without re-subscribing on every round update.
|
|
826
854
|
const [selectedRound, setSelectedRound] = useState(null);
|
|
855
|
+
// Mirrors councilStatuses for the global key handler: ctrl+e only belongs to
|
|
856
|
+
// the council todo panel while that panel is actually auto-collapsed, which
|
|
857
|
+
// app.tsx gates on `councilStatuses.length > 0`. Reading it from a ref keeps
|
|
858
|
+
// handleKey off the councilStatuses dep list.
|
|
859
|
+
const councilStatusesRef = useRef([]);
|
|
860
|
+
useEffect(() => {
|
|
861
|
+
councilStatusesRef.current = councilStatuses;
|
|
862
|
+
}, [councilStatuses]);
|
|
827
863
|
const councilRoundsRef = useRef([]);
|
|
828
864
|
useEffect(() => {
|
|
829
865
|
councilRoundsRef.current = councilRounds;
|
|
@@ -1018,6 +1054,10 @@ export function useAppLogic(props) {
|
|
|
1018
1054
|
// Pending resolvers for safety-override askcards. Keyed by questionId; populated
|
|
1019
1055
|
// by setSafetyOverrideHandler, drained by the askcard answer/cancel branches.
|
|
1020
1056
|
const safetyOverrideResolversRef = useRef(new Map());
|
|
1057
|
+
// Pending resolvers for ask_user askcards (the model-callable ask_user tool).
|
|
1058
|
+
// Keyed by questionId; populated by setAskUserHandler, drained by the askcard
|
|
1059
|
+
// answer/cancel branches. Resolve value = the human's answer string.
|
|
1060
|
+
const askUserResolversRef = useRef(new Map());
|
|
1021
1061
|
// Current todo snapshot (Claude-style sticky checklist). Updated by
|
|
1022
1062
|
// `task_list_update` chunks emitted after every `todo_write` tool call.
|
|
1023
1063
|
// Auto-hides ~2s after 100% completion so the panel doesn't linger.
|
|
@@ -1032,7 +1072,9 @@ export function useAppLogic(props) {
|
|
|
1032
1072
|
// throws (internal try/catch → []).
|
|
1033
1073
|
// biome-ignore lint/correctness/useExhaustiveDependencies: sessionId + messages.length are intentional recompute signals (getSessionTree reads the current session internally)
|
|
1034
1074
|
const sessionTree = useMemo(() => agent.getSessionTree(), [agent, sessionId, messages.length]);
|
|
1035
|
-
|
|
1075
|
+
// Boot straight to chat: no forced API-key modal on start. Auth is on-demand
|
|
1076
|
+
// via the provider picker (opens on a no-auth send, or via /providers).
|
|
1077
|
+
const [showApiKeyModal, setShowApiKeyModal] = useState(false);
|
|
1036
1078
|
const [apiKeyError, setApiKeyError] = useState(null);
|
|
1037
1079
|
const [showSlashMenu, setShowSlashMenu] = useState(false);
|
|
1038
1080
|
// Ref stays current synchronously so keyboard-burst handlers read the right value
|
|
@@ -1085,6 +1127,18 @@ export function useAppLogic(props) {
|
|
|
1085
1127
|
const newSinceLockRef = useRef(0);
|
|
1086
1128
|
const [newSinceLock, setNewSinceLock] = useState(0);
|
|
1087
1129
|
const [scrollLockedAway, setScrollLockedAway] = useState(false);
|
|
1130
|
+
// Transcript length when the user last left the bottom. The pill counts new
|
|
1131
|
+
// *entries* against this watermark. It must NOT count scrollToBottom() calls:
|
|
1132
|
+
// those fire per 32ms stream flush and per tool event, so a single streaming
|
|
1133
|
+
// answer used to push the pill into the hundreds ("251 new below").
|
|
1134
|
+
const lockedAtCountRef = useRef(0);
|
|
1135
|
+
// Ref mirror of scrollLockedAway so the scroll helpers can read the current
|
|
1136
|
+
// lock state synchronously (React state lags a render behind).
|
|
1137
|
+
const lockedAwayRef = useRef(false);
|
|
1138
|
+
const messageCountRef = useRef(0);
|
|
1139
|
+
useEffect(() => {
|
|
1140
|
+
messageCountRef.current = messages.length;
|
|
1141
|
+
}, [messages.length]);
|
|
1088
1142
|
// Context rail (MUONROI_CONTEXT_RAIL): user can hide/show the right metadata
|
|
1089
1143
|
// panel with Ctrl+B. Defaults visible; the rail also auto-hides below 100 cols
|
|
1090
1144
|
// (decided in app.tsx where terminal width is known).
|
|
@@ -1103,29 +1157,49 @@ export function useAppLogic(props) {
|
|
|
1103
1157
|
const vpH = sb.viewport.height ?? 0;
|
|
1104
1158
|
return sb.scrollTop + vpH >= sb.scrollHeight - 2;
|
|
1105
1159
|
}, []);
|
|
1160
|
+
// Retire the jump-to-latest pill. Single writer for the three lock fields so
|
|
1161
|
+
// the ref mirror can never drift from the rendered state.
|
|
1162
|
+
const clearScrollLock = useCallback(() => {
|
|
1163
|
+
lockedAwayRef.current = false;
|
|
1164
|
+
lockedAtCountRef.current = messageCountRef.current;
|
|
1165
|
+
if (newSinceLockRef.current !== 0) {
|
|
1166
|
+
newSinceLockRef.current = 0;
|
|
1167
|
+
setNewSinceLock(0);
|
|
1168
|
+
}
|
|
1169
|
+
setScrollLockedAway(false);
|
|
1170
|
+
}, []);
|
|
1171
|
+
// Engage the lock. Single writer, mirroring clearScrollLock. The first call
|
|
1172
|
+
// after leaving the bottom sets the watermark; later calls are no-ops, so
|
|
1173
|
+
// repeated scroll attempts for the SAME content (32ms stream flushes, tool
|
|
1174
|
+
// events) can never inflate the pill.
|
|
1175
|
+
const engageScrollLock = useCallback(() => {
|
|
1176
|
+
if (!isScrollLockEnabled() || lockedAwayRef.current)
|
|
1177
|
+
return;
|
|
1178
|
+
lockedAwayRef.current = true;
|
|
1179
|
+
lockedAtCountRef.current = messageCountRef.current;
|
|
1180
|
+
setScrollLockedAway(true);
|
|
1181
|
+
}, []);
|
|
1106
1182
|
// Soft scroll: respects scroll-lock. New content that arrives while the user
|
|
1107
1183
|
// reads history does NOT move the viewport; it just increments the pill count.
|
|
1108
1184
|
const scrollToBottom = useCallback(() => {
|
|
1109
1185
|
if (isScrollLockEnabled() && !isPinnedToBottom()) {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1186
|
+
engageScrollLock();
|
|
1187
|
+
const fresh = Math.max(0, messageCountRef.current - lockedAtCountRef.current);
|
|
1188
|
+
newSinceLockRef.current = fresh;
|
|
1189
|
+
setNewSinceLock(fresh);
|
|
1113
1190
|
return;
|
|
1114
1191
|
}
|
|
1115
1192
|
// Pinned (or lock off): scroll, and if the user had manually scrolled back
|
|
1116
1193
|
// to the bottom on their own, retire the jump-to-latest pill.
|
|
1117
|
-
if (newSinceLockRef.current !== 0)
|
|
1118
|
-
|
|
1119
|
-
setNewSinceLock(0);
|
|
1120
|
-
setScrollLockedAway(false);
|
|
1121
|
-
}
|
|
1194
|
+
if (lockedAwayRef.current || newSinceLockRef.current !== 0)
|
|
1195
|
+
clearScrollLock();
|
|
1122
1196
|
try {
|
|
1123
1197
|
scrollRef.current?.scrollTo(scrollRef.current?.scrollHeight ?? 99999);
|
|
1124
1198
|
}
|
|
1125
1199
|
catch {
|
|
1126
1200
|
/* */
|
|
1127
1201
|
}
|
|
1128
|
-
}, [isPinnedToBottom]);
|
|
1202
|
+
}, [isPinnedToBottom, clearScrollLock, engageScrollLock]);
|
|
1129
1203
|
// Hard scroll: re-arms native sticky and jumps to the latest line regardless
|
|
1130
1204
|
// of manual-scroll state. Used by explicit user actions (new prompt submit,
|
|
1131
1205
|
// jump-to-latest pill) that intend to return to the live tail.
|
|
@@ -1141,10 +1215,8 @@ export function useAppLogic(props) {
|
|
|
1141
1215
|
/* */
|
|
1142
1216
|
}
|
|
1143
1217
|
}
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
setScrollLockedAway(false);
|
|
1147
|
-
}, []);
|
|
1218
|
+
clearScrollLock();
|
|
1219
|
+
}, [clearScrollLock]);
|
|
1148
1220
|
const { width, height } = useTerminalDimensions();
|
|
1149
1221
|
const processedInitial = useRef(false);
|
|
1150
1222
|
const contentAccRef = useRef("");
|
|
@@ -1156,7 +1228,7 @@ export function useAppLogic(props) {
|
|
|
1156
1228
|
const originalIdealPromptRef = useRef(null);
|
|
1157
1229
|
const isProcessingRef = useRef(false);
|
|
1158
1230
|
const hasApiKeyRef = useRef(initialHasApiKey);
|
|
1159
|
-
const showApiKeyModalRef = useRef(
|
|
1231
|
+
const showApiKeyModalRef = useRef(false);
|
|
1160
1232
|
const queuedMessagesRef = useRef([]);
|
|
1161
1233
|
const processMessageRef = useRef(() => { });
|
|
1162
1234
|
const [queuedMessages, setQueuedMessages] = useState([]);
|
|
@@ -1789,6 +1861,16 @@ export function useAppLogic(props) {
|
|
|
1789
1861
|
setCouncilRounds([]);
|
|
1790
1862
|
setSelectedRound(null);
|
|
1791
1863
|
setCouncilPlaceholders(new Map());
|
|
1864
|
+
// The auto-council path drives councilPhases through the MAIN stream loop,
|
|
1865
|
+
// which — unlike the nested /council and /ideal loops — never reset it on
|
|
1866
|
+
// turn end. On a hung/watchdog-aborted council turn the running phase never
|
|
1867
|
+
// emits its `state:"done"` event, so <CouncilPhaseTimeline> stays frozen at
|
|
1868
|
+
// "Council working… elapsed Ns" forever and the session LOOKS hung even after
|
|
1869
|
+
// the turn actually finalized (live: reasoning-model hang c1d461439618).
|
|
1870
|
+
// Clearing here (called at both beginLiveTurn and finalizeActiveTurn) tears
|
|
1871
|
+
// the timeline down on every turn boundary. The completed phases were already
|
|
1872
|
+
// rendered live; the persisted [Council Decision] message is the durable record.
|
|
1873
|
+
setCouncilPhases([]);
|
|
1792
1874
|
councilTopicRef.current = undefined;
|
|
1793
1875
|
}, []);
|
|
1794
1876
|
const finishTurnProcessing = useCallback(() => {
|
|
@@ -2102,6 +2184,39 @@ export function useAppLogic(props) {
|
|
|
2102
2184
|
safetyOverrideResolversRef.current.clear();
|
|
2103
2185
|
};
|
|
2104
2186
|
}, [agent, setPendingCouncilQuestionSync, setCouncilCardStateSync]);
|
|
2187
|
+
// ask_user handler — surfaces an AGENT-authored card (question + options come
|
|
2188
|
+
// ONLY from the model's tool input; the CLI synthesises nothing) and blocks
|
|
2189
|
+
// until the human answers or dismisses. The resolved string becomes the tool
|
|
2190
|
+
// result the agent reads to decide its next step.
|
|
2191
|
+
useEffect(() => {
|
|
2192
|
+
agent.setAskUserHandler(async (info) => {
|
|
2193
|
+
return new Promise((resolve) => {
|
|
2194
|
+
const qid = `ask-user-${Date.now()}`;
|
|
2195
|
+
askUserResolversRef.current.set(qid, resolve);
|
|
2196
|
+
const question = buildAskUserQuestion(info, qid);
|
|
2197
|
+
setPendingCouncilQuestionSync(question);
|
|
2198
|
+
setCouncilCardStateSync(initialCardState(question));
|
|
2199
|
+
try {
|
|
2200
|
+
agentRuntime?.emitEvent({
|
|
2201
|
+
t: "event",
|
|
2202
|
+
kind: "askcard-open",
|
|
2203
|
+
questionId: qid,
|
|
2204
|
+
question: question.question,
|
|
2205
|
+
phase: "ask-user",
|
|
2206
|
+
optionCount: question.options?.length ?? 0,
|
|
2207
|
+
defaultIndex: question.defaultIndex ?? 0,
|
|
2208
|
+
});
|
|
2209
|
+
}
|
|
2210
|
+
catch {
|
|
2211
|
+
/* best-effort */
|
|
2212
|
+
}
|
|
2213
|
+
});
|
|
2214
|
+
});
|
|
2215
|
+
return () => {
|
|
2216
|
+
agent.setAskUserHandler(null);
|
|
2217
|
+
askUserResolversRef.current.clear();
|
|
2218
|
+
};
|
|
2219
|
+
}, [agent, setPendingCouncilQuestionSync, setCouncilCardStateSync]);
|
|
2105
2220
|
const finalizeActiveTurn = useCallback(({ wasInterrupted = false, hadError = false } = {}) => {
|
|
2106
2221
|
const activeTurn = activeTurnRef.current;
|
|
2107
2222
|
if (!activeTurn) {
|
|
@@ -2134,7 +2249,11 @@ export function useAppLogic(props) {
|
|
|
2134
2249
|
prevUserIdx++;
|
|
2135
2250
|
}
|
|
2136
2251
|
}
|
|
2137
|
-
|
|
2252
|
+
// The persisted transcript has one tool_result per call and no
|
|
2253
|
+
// tool_group, so this resync used to wipe the group the live turn
|
|
2254
|
+
// built and leave a flat "→ <tool>" line per call on screen — the
|
|
2255
|
+
// state the user actually reads after the answer lands. Re-fold them.
|
|
2256
|
+
return groupToolEntries(fresh);
|
|
2138
2257
|
});
|
|
2139
2258
|
setSessionTitle(activeTurn.agent.getSessionTitle());
|
|
2140
2259
|
setSessionId(activeTurn.agent.getSessionId());
|
|
@@ -2691,6 +2810,22 @@ export function useAppLogic(props) {
|
|
|
2691
2810
|
showApiKeyModal,
|
|
2692
2811
|
showSlashMenu,
|
|
2693
2812
|
]);
|
|
2813
|
+
// Retire the pill when the user scrolls back to the bottom BY HAND (mouse
|
|
2814
|
+
// wheel / trackpad). Previously only End (scrollToBottomForced) or new content
|
|
2815
|
+
// arriving while pinned could clear it, so wheeling down to the newest line
|
|
2816
|
+
// left a stale "N new below" sitting there — the reported bug.
|
|
2817
|
+
// ScrollBoxRenderable exposes no scroll event (checked @opentui/core 0.1.107:
|
|
2818
|
+
// scrollTo/scrollBy/scrollTop, no emitter), so a poll is the only hook. It
|
|
2819
|
+
// runs ONLY while the pill is up, and stops the moment it clears.
|
|
2820
|
+
useEffect(() => {
|
|
2821
|
+
if (!scrollLockedAway)
|
|
2822
|
+
return;
|
|
2823
|
+
const id = setInterval(() => {
|
|
2824
|
+
if (isPinnedToBottom())
|
|
2825
|
+
clearScrollLock();
|
|
2826
|
+
}, 200);
|
|
2827
|
+
return () => clearInterval(id);
|
|
2828
|
+
}, [scrollLockedAway, isPinnedToBottom, clearScrollLock]);
|
|
2694
2829
|
useEffect(() => {
|
|
2695
2830
|
const onRawInput = (sequence) => {
|
|
2696
2831
|
const parsed = parseKeypress(sequence, { useKittyKeyboard: renderer.useKittyKeyboard });
|
|
@@ -2718,7 +2853,7 @@ export function useAppLogic(props) {
|
|
|
2718
2853
|
}, [interruptActiveRun, renderer]);
|
|
2719
2854
|
const resetToNewSession = useCallback(() => {
|
|
2720
2855
|
const snapshot = agent.startNewSession();
|
|
2721
|
-
setMessages(snapshot?.entries ?? []);
|
|
2856
|
+
setMessages(groupToolEntries(snapshot?.entries ?? []));
|
|
2722
2857
|
setExpandedMessages(new Set());
|
|
2723
2858
|
activeTurnRef.current = null;
|
|
2724
2859
|
clearLiveTurnUi();
|
|
@@ -2987,14 +3122,10 @@ export function useAppLogic(props) {
|
|
|
2987
3122
|
}
|
|
2988
3123
|
setCouncilStatuses((prev) => upsertStatus(prev, cs));
|
|
2989
3124
|
// Task 2.2b — emit council-speaker harness event (agent-mode only).
|
|
3125
|
+
// Carries state:"tick" + elapsedMs so a harness poller can tell
|
|
3126
|
+
// a long-running research phase (alive) from a hung one.
|
|
2990
3127
|
try {
|
|
2991
|
-
agentRuntime?.emitEvent(
|
|
2992
|
-
t: "event",
|
|
2993
|
-
kind: "council-speaker",
|
|
2994
|
-
role: cs.role ?? cs.label ?? "unknown",
|
|
2995
|
-
status: cs.state === "start" ? "start" : "done",
|
|
2996
|
-
correlationId: cs.statusId,
|
|
2997
|
-
});
|
|
3128
|
+
agentRuntime?.emitEvent(mapCouncilStatusToSpeakerEvent(cs));
|
|
2998
3129
|
}
|
|
2999
3130
|
catch {
|
|
3000
3131
|
/* best-effort */
|
|
@@ -3160,7 +3291,7 @@ export function useAppLogic(props) {
|
|
|
3160
3291
|
appendCompaction(sessionId, nextSeq, cr.summary, cr.tokensBeforeCompress);
|
|
3161
3292
|
}
|
|
3162
3293
|
agent.setMessages([createCompactionSummaryMessage(cr.summary)]);
|
|
3163
|
-
setMessages(agent.getChatEntries());
|
|
3294
|
+
setMessages(groupToolEntries(agent.getChatEntries()));
|
|
3164
3295
|
setMessages((prev) => [
|
|
3165
3296
|
...prev,
|
|
3166
3297
|
buildAssistantEntry(`⋯ Đã tự nén ngữ cảnh (${cr.tokensBeforeCompress} → ${cr.tokensAfterCompress} tokens, giữ ${cr.decisionsExtracted} quyết định; kết quả tool vẫn rehydrate được qua ee_query). Tiếp tục tác vụ...`),
|
|
@@ -3533,8 +3664,10 @@ export function useAppLogic(props) {
|
|
|
3533
3664
|
const match = result.match(/Instructions:\s*(.+)/);
|
|
3534
3665
|
const instructions = match ? match[1].trim() : "";
|
|
3535
3666
|
const flowDir = path.join(agent.getCwd(), ".muonroi-flow");
|
|
3667
|
+
const startedAt = Date.now();
|
|
3668
|
+
setCompactRun({ progress: stageProgress("artifacts"), startedAt });
|
|
3536
3669
|
try {
|
|
3537
|
-
const cr = await deliberateCompact(flowDir, agent.getMessages(), "", 4096,
|
|
3670
|
+
const cr = await deliberateCompact(flowDir, agent.getMessages(), "", 4096, model, instructions, (p) => setCompactRun((prev) => (prev ? { ...prev, progress: p } : prev)));
|
|
3538
3671
|
const sessionId = agent.getSessionId();
|
|
3539
3672
|
if (sessionId) {
|
|
3540
3673
|
const nextSeq = getNextMessageSequence(sessionId);
|
|
@@ -3542,7 +3675,7 @@ export function useAppLogic(props) {
|
|
|
3542
3675
|
}
|
|
3543
3676
|
const summaryMsg = createCompactionSummaryMessage(cr.summary);
|
|
3544
3677
|
agent.setMessages([summaryMsg]);
|
|
3545
|
-
setMessages(agent.getChatEntries());
|
|
3678
|
+
setMessages(groupToolEntries(agent.getChatEntries()));
|
|
3546
3679
|
setMessages((prev) => [
|
|
3547
3680
|
...prev,
|
|
3548
3681
|
buildAssistantEntry(`Compaction: ${cr.decisionsExtracted} decisions extracted, ${cr.tokensBeforeCompress} → ${cr.tokensAfterCompress} tokens.\nSnapshot: ${cr.historyPath}`),
|
|
@@ -3551,6 +3684,9 @@ export function useAppLogic(props) {
|
|
|
3551
3684
|
catch (e) {
|
|
3552
3685
|
setMessages((prev) => [...prev, buildAssistantEntry(`Compaction failed: ${e}`)]);
|
|
3553
3686
|
}
|
|
3687
|
+
finally {
|
|
3688
|
+
setCompactRun(null);
|
|
3689
|
+
}
|
|
3554
3690
|
return;
|
|
3555
3691
|
}
|
|
3556
3692
|
if (result.startsWith("__EXPAND__") || result.startsWith("__EXPAND_JSON__")) {
|
|
@@ -3564,7 +3700,7 @@ export function useAppLogic(props) {
|
|
|
3564
3700
|
revertLatestCompaction(sessionId);
|
|
3565
3701
|
}
|
|
3566
3702
|
agent.setMessages(restoredMessages);
|
|
3567
|
-
setMessages(agent.getChatEntries());
|
|
3703
|
+
setMessages(groupToolEntries(agent.getChatEntries()));
|
|
3568
3704
|
const text = lines.slice(2).join("\n");
|
|
3569
3705
|
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
3570
3706
|
return;
|
|
@@ -3807,13 +3943,7 @@ export function useAppLogic(props) {
|
|
|
3807
3943
|
setCouncilStatuses((prev) => upsertStatus(prev, cs));
|
|
3808
3944
|
// Task 2.2b — emit council-speaker in branch 2 (agent-mode only).
|
|
3809
3945
|
try {
|
|
3810
|
-
agentRuntime?.emitEvent(
|
|
3811
|
-
t: "event",
|
|
3812
|
-
kind: "council-speaker",
|
|
3813
|
-
role: cs.role ?? cs.label ?? "unknown",
|
|
3814
|
-
status: cs.state === "start" ? "start" : "done",
|
|
3815
|
-
correlationId: cs.statusId,
|
|
3816
|
-
});
|
|
3946
|
+
agentRuntime?.emitEvent(mapCouncilStatusToSpeakerEvent(cs));
|
|
3817
3947
|
}
|
|
3818
3948
|
catch {
|
|
3819
3949
|
/* best-effort */
|
|
@@ -3982,7 +4112,7 @@ export function useAppLogic(props) {
|
|
|
3982
4112
|
isProcessingRef.current = true;
|
|
3983
4113
|
setIsProcessing(true);
|
|
3984
4114
|
try {
|
|
3985
|
-
const gen = agent.runCouncilV2(topic);
|
|
4115
|
+
const gen = agent.runCouncilV2(topic, { convenePath: true });
|
|
3986
4116
|
for await (const chunk of gen) {
|
|
3987
4117
|
// Council emitted a chunk — clear the "Waiting for next phase"
|
|
3988
4118
|
// inter-card heartbeat started after the last askcard answer.
|
|
@@ -4102,13 +4232,7 @@ export function useAppLogic(props) {
|
|
|
4102
4232
|
setCouncilStatuses((prev) => upsertStatus(prev, cs));
|
|
4103
4233
|
// Task 2.2b — emit council-speaker in branch 3 (agent-mode only).
|
|
4104
4234
|
try {
|
|
4105
|
-
agentRuntime?.emitEvent(
|
|
4106
|
-
t: "event",
|
|
4107
|
-
kind: "council-speaker",
|
|
4108
|
-
role: cs.role ?? cs.label ?? "unknown",
|
|
4109
|
-
status: cs.state === "start" ? "start" : "done",
|
|
4110
|
-
correlationId: cs.statusId,
|
|
4111
|
-
});
|
|
4235
|
+
agentRuntime?.emitEvent(mapCouncilStatusToSpeakerEvent(cs));
|
|
4112
4236
|
}
|
|
4113
4237
|
catch {
|
|
4114
4238
|
/* best-effort */
|
|
@@ -4354,8 +4478,10 @@ export function useAppLogic(props) {
|
|
|
4354
4478
|
const match = result.match(/Instructions:\s*(.+)/);
|
|
4355
4479
|
const instructions = match ? match[1].trim() : "";
|
|
4356
4480
|
const flowDir = path.join(agent.getCwd(), ".muonroi-flow");
|
|
4481
|
+
const startedAt = Date.now();
|
|
4482
|
+
setCompactRun({ progress: stageProgress("artifacts"), startedAt });
|
|
4357
4483
|
try {
|
|
4358
|
-
const cr = await deliberateCompact(flowDir, agent.getMessages(), "", 4096,
|
|
4484
|
+
const cr = await deliberateCompact(flowDir, agent.getMessages(), "", 4096, model, instructions, (p) => setCompactRun((prev) => (prev ? { ...prev, progress: p } : prev)));
|
|
4359
4485
|
const sessionId = agent.getSessionId();
|
|
4360
4486
|
if (sessionId) {
|
|
4361
4487
|
const nextSeq = getNextMessageSequence(sessionId);
|
|
@@ -4363,7 +4489,7 @@ export function useAppLogic(props) {
|
|
|
4363
4489
|
}
|
|
4364
4490
|
const summaryMsg = createCompactionSummaryMessage(cr.summary);
|
|
4365
4491
|
agent.setMessages([summaryMsg]);
|
|
4366
|
-
setMessages(agent.getChatEntries());
|
|
4492
|
+
setMessages(groupToolEntries(agent.getChatEntries()));
|
|
4367
4493
|
setMessages((prev) => [
|
|
4368
4494
|
...prev,
|
|
4369
4495
|
buildAssistantEntry(`Compaction: ${cr.decisionsExtracted} decisions extracted, ${cr.tokensBeforeCompress} → ${cr.tokensAfterCompress} tokens.\nSnapshot: ${cr.historyPath}`),
|
|
@@ -4372,6 +4498,9 @@ export function useAppLogic(props) {
|
|
|
4372
4498
|
catch (e) {
|
|
4373
4499
|
setMessages((prev) => [...prev, buildAssistantEntry(`Compaction failed: ${e}`)]);
|
|
4374
4500
|
}
|
|
4501
|
+
finally {
|
|
4502
|
+
setCompactRun(null);
|
|
4503
|
+
}
|
|
4375
4504
|
return;
|
|
4376
4505
|
}
|
|
4377
4506
|
if (result.startsWith("__EXPAND__") || result.startsWith("__EXPAND_JSON__")) {
|
|
@@ -4385,7 +4514,7 @@ export function useAppLogic(props) {
|
|
|
4385
4514
|
revertLatestCompaction(sessionId);
|
|
4386
4515
|
}
|
|
4387
4516
|
agent.setMessages(restoredMessages);
|
|
4388
|
-
setMessages(agent.getChatEntries());
|
|
4517
|
+
setMessages(groupToolEntries(agent.getChatEntries()));
|
|
4389
4518
|
const text = lines.slice(2).join("\n");
|
|
4390
4519
|
setMessages((prev) => [...prev, buildAssistantEntry(text)]);
|
|
4391
4520
|
return;
|
|
@@ -4628,9 +4757,12 @@ export function useAppLogic(props) {
|
|
|
4628
4757
|
}
|
|
4629
4758
|
// Ctrl+E — peek the full todo panel while it is auto-collapsed during a
|
|
4630
4759
|
// council debate (app.tsx collapses it to one line so the debate scrollbox
|
|
4631
|
-
// keeps its height).
|
|
4632
|
-
//
|
|
4633
|
-
|
|
4760
|
+
// keeps its height). Scoped to an ACTIVE council: app.tsx only collapses
|
|
4761
|
+
// the panel when `councilStatuses.length > 0`, so outside a debate this
|
|
4762
|
+
// toggle is invisible — and an unconditional `return` here swallowed the
|
|
4763
|
+
// key before the transcript expand handler below could ever see it,
|
|
4764
|
+
// making every "ctrl+e expand" affordance in the message log dead.
|
|
4765
|
+
if (key.name === "e" && key.ctrl && !key.meta && councilStatusesRef.current.length > 0) {
|
|
4634
4766
|
setCouncilTodoExpanded((v) => !v);
|
|
4635
4767
|
return;
|
|
4636
4768
|
}
|
|
@@ -4678,11 +4810,16 @@ export function useAppLogic(props) {
|
|
|
4678
4810
|
/* */
|
|
4679
4811
|
}
|
|
4680
4812
|
// Paging up leaves the tail; paging back to bottom clears the pill.
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4813
|
+
// Engaging on pageup is what was missing: the lock was only ever set
|
|
4814
|
+
// from scrollToBottom(), i.e. when new content ARRIVED while away
|
|
4815
|
+
// from the tail. Paging up therefore surfaced the pill only if a
|
|
4816
|
+
// render happened to follow — measured as engaging on one run and
|
|
4817
|
+
// not the next. Setting it here makes it depend on the key, not on
|
|
4818
|
+
// whether the stream was still flushing.
|
|
4819
|
+
if (key.name === "pageup" && !isPinnedToBottom())
|
|
4820
|
+
engageScrollLock();
|
|
4821
|
+
if (key.name === "pagedown" && isPinnedToBottom())
|
|
4822
|
+
clearScrollLock();
|
|
4686
4823
|
}
|
|
4687
4824
|
return;
|
|
4688
4825
|
}
|
|
@@ -5183,6 +5320,32 @@ export function useAppLogic(props) {
|
|
|
5183
5320
|
});
|
|
5184
5321
|
return;
|
|
5185
5322
|
}
|
|
5323
|
+
// ask_user askcard: resolve the stored promise with the user's
|
|
5324
|
+
// answer string (option value or free text) — becomes the tool result.
|
|
5325
|
+
if (pendingQuestion.phase === "ask-user") {
|
|
5326
|
+
const resolver = askUserResolversRef.current.get(qid);
|
|
5327
|
+
askUserResolversRef.current.delete(qid);
|
|
5328
|
+
setPendingCouncilQuestionSync(null);
|
|
5329
|
+
setCouncilCardStateSync(null);
|
|
5330
|
+
resolver?.(ans.text);
|
|
5331
|
+
try {
|
|
5332
|
+
agentRuntime?.emitEvent({
|
|
5333
|
+
t: "event",
|
|
5334
|
+
kind: "askcard-answered",
|
|
5335
|
+
questionId: qid,
|
|
5336
|
+
answerKind: ans.kind ?? "choice",
|
|
5337
|
+
answerText: ans.text,
|
|
5338
|
+
});
|
|
5339
|
+
}
|
|
5340
|
+
catch {
|
|
5341
|
+
/* best-effort */
|
|
5342
|
+
}
|
|
5343
|
+
logUIInteraction(agent.getSessionId() ?? undefined, {
|
|
5344
|
+
subtype: "askcard_answered",
|
|
5345
|
+
data: { questionId: qid, answerKind: ans.kind ?? "choice", answerText: ans.text },
|
|
5346
|
+
});
|
|
5347
|
+
return;
|
|
5348
|
+
}
|
|
5186
5349
|
// Resolve the label of the option that was selected. For "choice"
|
|
5187
5350
|
// and "freetext" the option index lives on the card state; for
|
|
5188
5351
|
// "chat" the user typed a free reply and there is no option to
|
|
@@ -5270,6 +5433,23 @@ export function useAppLogic(props) {
|
|
|
5270
5433
|
}
|
|
5271
5434
|
return;
|
|
5272
5435
|
}
|
|
5436
|
+
// ask_user cancel (Esc) → resolve with the dismissed sentinel; the
|
|
5437
|
+
// agent reads it and decides its own follow-up (never CLI-forced).
|
|
5438
|
+
if (pendingQuestion.phase === "ask-user") {
|
|
5439
|
+
const resolver = askUserResolversRef.current.get(qid);
|
|
5440
|
+
askUserResolversRef.current.delete(qid);
|
|
5441
|
+
setPendingCouncilQuestionSync(null);
|
|
5442
|
+
setCouncilCardStateSync(null);
|
|
5443
|
+
clearInterCardHeartbeat();
|
|
5444
|
+
resolver?.(ASK_USER_DISMISSED);
|
|
5445
|
+
try {
|
|
5446
|
+
agentRuntime?.emitEvent({ t: "event", kind: "askcard-cancel", questionId: qid });
|
|
5447
|
+
}
|
|
5448
|
+
catch {
|
|
5449
|
+
/* best-effort */
|
|
5450
|
+
}
|
|
5451
|
+
return;
|
|
5452
|
+
}
|
|
5273
5453
|
setPendingCouncilQuestionSync(null);
|
|
5274
5454
|
setCouncilCardStateSync(null);
|
|
5275
5455
|
clearInterCardHeartbeat();
|
|
@@ -5848,61 +6028,11 @@ export function useAppLogic(props) {
|
|
|
5848
6028
|
return;
|
|
5849
6029
|
}
|
|
5850
6030
|
if (showModelPicker) {
|
|
5851
|
-
// Sub-modal:
|
|
5852
|
-
|
|
6031
|
+
// Sub-modal: OAuth login in progress (browser-based). Only Esc (cancel)
|
|
6032
|
+
// is actionable — the flow completes via the browser/loopback callback.
|
|
6033
|
+
if (oauthLogin) {
|
|
5853
6034
|
if (isEscapeKey(key)) {
|
|
5854
|
-
|
|
5855
|
-
return;
|
|
5856
|
-
}
|
|
5857
|
-
if (bwSync.phase === "password") {
|
|
5858
|
-
if (bwSync.loading)
|
|
5859
|
-
return;
|
|
5860
|
-
if (key.name === "return") {
|
|
5861
|
-
void submitBwPassword();
|
|
5862
|
-
return;
|
|
5863
|
-
}
|
|
5864
|
-
if (key.name === "backspace") {
|
|
5865
|
-
// Functional updater — burst-safe (see API-key prompt above).
|
|
5866
|
-
setBwSync((bw) => bw && bw.phase === "password" ? { ...bw, value: bw.value.slice(0, -1), error: null } : bw);
|
|
5867
|
-
return;
|
|
5868
|
-
}
|
|
5869
|
-
if (key.sequence && !key.ctrl && !key.meta) {
|
|
5870
|
-
// Keep spaces: a master password may legitimately contain them.
|
|
5871
|
-
const cleaned = stripControlBytes(key.sequence);
|
|
5872
|
-
if (cleaned.length === 0)
|
|
5873
|
-
return;
|
|
5874
|
-
setBwSync((bw) => bw && bw.phase === "password" ? { ...bw, value: bw.value + cleaned, error: null } : bw);
|
|
5875
|
-
return;
|
|
5876
|
-
}
|
|
5877
|
-
return;
|
|
5878
|
-
}
|
|
5879
|
-
if (bwSync.phase === "picker") {
|
|
5880
|
-
if (bwSync.loading)
|
|
5881
|
-
return;
|
|
5882
|
-
if (key.name === "up") {
|
|
5883
|
-
setBwSync({ ...bwSync, focusIndex: Math.max(0, bwSync.focusIndex - 1) });
|
|
5884
|
-
return;
|
|
5885
|
-
}
|
|
5886
|
-
if (key.name === "down") {
|
|
5887
|
-
setBwSync({ ...bwSync, focusIndex: Math.min(bwSync.items.length - 1, bwSync.focusIndex + 1) });
|
|
5888
|
-
return;
|
|
5889
|
-
}
|
|
5890
|
-
if (key.name === "space" || key.sequence === " ") {
|
|
5891
|
-
const item = bwSync.items[bwSync.focusIndex];
|
|
5892
|
-
if (item) {
|
|
5893
|
-
const next = new Set(bwSync.selected);
|
|
5894
|
-
if (next.has(item.provider))
|
|
5895
|
-
next.delete(item.provider);
|
|
5896
|
-
else
|
|
5897
|
-
next.add(item.provider);
|
|
5898
|
-
setBwSync({ ...bwSync, selected: next });
|
|
5899
|
-
}
|
|
5900
|
-
return;
|
|
5901
|
-
}
|
|
5902
|
-
if (key.name === "return") {
|
|
5903
|
-
void commitBwImport();
|
|
5904
|
-
return;
|
|
5905
|
-
}
|
|
6035
|
+
cancelProviderOAuth();
|
|
5906
6036
|
return;
|
|
5907
6037
|
}
|
|
5908
6038
|
return;
|
|
@@ -5963,8 +6093,10 @@ export function useAppLogic(props) {
|
|
|
5963
6093
|
setApiKeyPrompt({ provider: p, value: "", error: null });
|
|
5964
6094
|
return;
|
|
5965
6095
|
}
|
|
5966
|
-
if (key.name === "
|
|
5967
|
-
|
|
6096
|
+
if (key.name === "o") {
|
|
6097
|
+
const p = configuredProviders[providerChipIndex];
|
|
6098
|
+
if (p && oauthProviders.has(p))
|
|
6099
|
+
void startProviderOAuth(p);
|
|
5968
6100
|
return;
|
|
5969
6101
|
}
|
|
5970
6102
|
if (key.name === "space" || key.sequence === " ") {
|
|
@@ -5975,8 +6107,17 @@ export function useAppLogic(props) {
|
|
|
5975
6107
|
}
|
|
5976
6108
|
if (key.name === "d" || key.name === "return") {
|
|
5977
6109
|
const p = configuredProviders[providerChipIndex];
|
|
5978
|
-
if (p
|
|
6110
|
+
if (!p)
|
|
6111
|
+
return;
|
|
6112
|
+
// Enter means "use this provider". With credentials that means making
|
|
6113
|
+
// it the default; with none it means signing in, which is the only
|
|
6114
|
+
// useful thing left to do on the row (Enter was a dead key there).
|
|
6115
|
+
// /login is gone, so this picker is the single auth surface: K adds a
|
|
6116
|
+
// key, Enter signs in via OAuth where the provider supports it.
|
|
6117
|
+
if (providersWithKey.has(p))
|
|
5979
6118
|
setAsDefaultProvider(p);
|
|
6119
|
+
else if (oauthProviders.has(p))
|
|
6120
|
+
void startProviderOAuth(p);
|
|
5980
6121
|
return;
|
|
5981
6122
|
}
|
|
5982
6123
|
return;
|
|
@@ -6188,10 +6329,8 @@ export function useAppLogic(props) {
|
|
|
6188
6329
|
key.stopPropagation();
|
|
6189
6330
|
return;
|
|
6190
6331
|
}
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
return;
|
|
6194
|
-
}
|
|
6332
|
+
// No forced modal on keystroke when unauthenticated — the user can type
|
|
6333
|
+
// freely; the provider picker opens on send instead (see handleSubmit).
|
|
6195
6334
|
if (key.sequence === "/" && !isProcessing) {
|
|
6196
6335
|
const text = inputRef.current?.plainText || "";
|
|
6197
6336
|
if (!text.trim()) {
|
|
@@ -6208,20 +6347,18 @@ export function useAppLogic(props) {
|
|
|
6208
6347
|
}
|
|
6209
6348
|
}
|
|
6210
6349
|
if (key.name === "e" && key.ctrl) {
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6215
|
-
|
|
6216
|
-
|
|
6217
|
-
}
|
|
6218
|
-
if (lastUserIdx >= 0) {
|
|
6350
|
+
// Toggle the NEWEST entry that actually renders a "ctrl+e" affordance —
|
|
6351
|
+
// long user text, collapsed model narration, chain-of-thought, or a done
|
|
6352
|
+
// tool group. Targeting only the last *user* message meant the key did
|
|
6353
|
+
// nothing for every other affordance that advertises it.
|
|
6354
|
+
const idx = findLastCollapsibleIndex(messages);
|
|
6355
|
+
if (idx >= 0) {
|
|
6219
6356
|
setExpandedMessages((prev) => {
|
|
6220
6357
|
const next = new Set(prev);
|
|
6221
|
-
if (next.has(
|
|
6222
|
-
next.delete(
|
|
6358
|
+
if (next.has(idx))
|
|
6359
|
+
next.delete(idx);
|
|
6223
6360
|
else
|
|
6224
|
-
next.add(
|
|
6361
|
+
next.add(idx);
|
|
6225
6362
|
return next;
|
|
6226
6363
|
});
|
|
6227
6364
|
}
|
|
@@ -6418,10 +6555,11 @@ export function useAppLogic(props) {
|
|
|
6418
6555
|
// these directly; omitting them froze the closure on a stale `null`, which
|
|
6419
6556
|
// is why typing/pasting into the per-provider key prompt did nothing.
|
|
6420
6557
|
apiKeyPrompt,
|
|
6421
|
-
|
|
6558
|
+
oauthLogin,
|
|
6559
|
+
oauthProviders,
|
|
6422
6560
|
submitProviderKey,
|
|
6423
|
-
|
|
6424
|
-
|
|
6561
|
+
startProviderOAuth,
|
|
6562
|
+
cancelProviderOAuth,
|
|
6425
6563
|
openApiKeyModal,
|
|
6426
6564
|
openCatalogMcp,
|
|
6427
6565
|
openMcpEditor,
|
|
@@ -6473,7 +6611,6 @@ export function useAppLogic(props) {
|
|
|
6473
6611
|
toggleModelDisabled,
|
|
6474
6612
|
pointToExistingForm,
|
|
6475
6613
|
setApiKeyPrompt,
|
|
6476
|
-
setBwSync,
|
|
6477
6614
|
setCouncilCardStateSync,
|
|
6478
6615
|
setPendingCouncilQuestionSync,
|
|
6479
6616
|
setShowSlashMenuSync,
|
|
@@ -6491,6 +6628,7 @@ export function useAppLogic(props) {
|
|
|
6491
6628
|
setSessionPickerIndex,
|
|
6492
6629
|
scrollToBottomForced,
|
|
6493
6630
|
isPinnedToBottom,
|
|
6631
|
+
clearScrollLock,
|
|
6494
6632
|
]);
|
|
6495
6633
|
useKeyboard(handleKey);
|
|
6496
6634
|
const handlePaste = useCallback((event) => {
|
|
@@ -6504,21 +6642,6 @@ export function useAppLogic(props) {
|
|
|
6504
6642
|
setApiKeyPrompt((s) => (s ? { ...s, value: s.value + pasted, error: null } : s));
|
|
6505
6643
|
return;
|
|
6506
6644
|
}
|
|
6507
|
-
if (bwSync && bwSync.phase === "password") {
|
|
6508
|
-
event.preventDefault();
|
|
6509
|
-
// Master passwords may contain spaces — keep them; only drop guards,
|
|
6510
|
-
// control bytes and line breaks (which a trailing paste newline adds).
|
|
6511
|
-
const pasted = stripControlBytes(decodePasteBytes(event.bytes)).replace(/[\r\n]+/g, "");
|
|
6512
|
-
if (pasted) {
|
|
6513
|
-
setBwSync((s) => (s && s.phase === "password" ? { ...s, value: s.value + pasted, error: null } : s));
|
|
6514
|
-
}
|
|
6515
|
-
return;
|
|
6516
|
-
}
|
|
6517
|
-
if (!hasApiKeyRef.current) {
|
|
6518
|
-
event.preventDefault();
|
|
6519
|
-
openApiKeyModal();
|
|
6520
|
-
return;
|
|
6521
|
-
}
|
|
6522
6645
|
const text = decodePasteBytes(event.bytes);
|
|
6523
6646
|
const trimmed = text.trim();
|
|
6524
6647
|
const imageExts = /\.(png|jpe?g|gif|webp|svg|bmp|ico|tiff?)$/i;
|
|
@@ -6538,7 +6661,7 @@ export function useAppLogic(props) {
|
|
|
6538
6661
|
const block = { id, content: text, lines: lineCount };
|
|
6539
6662
|
replacePasteBlocks([...pasteBlocksRef.current, block]);
|
|
6540
6663
|
inputRef.current?.insertText(getPasteBlockToken(block));
|
|
6541
|
-
}, [apiKeyPrompt,
|
|
6664
|
+
}, [apiKeyPrompt, replacePasteBlocks]);
|
|
6542
6665
|
const handleSubmit = useCallback(() => {
|
|
6543
6666
|
const raw = inputRef.current?.plainText || "";
|
|
6544
6667
|
if (raw.trim()) {
|
|
@@ -6562,6 +6685,14 @@ export function useAppLogic(props) {
|
|
|
6562
6685
|
}
|
|
6563
6686
|
return;
|
|
6564
6687
|
}
|
|
6688
|
+
// No provider configured yet: open the picker so the user can sign in
|
|
6689
|
+
// (OAuth) or add a key. Keep the composer text so the same message can be
|
|
6690
|
+
// resent as soon as a provider is connected.
|
|
6691
|
+
if (!hasApiKeyRef.current) {
|
|
6692
|
+
setShowModelPicker(true);
|
|
6693
|
+
setModelPickerIndex(0);
|
|
6694
|
+
return;
|
|
6695
|
+
}
|
|
6565
6696
|
inputRef.current?.clear();
|
|
6566
6697
|
let message = raw;
|
|
6567
6698
|
const blocks = [...pasteBlocksRef.current];
|
|
@@ -6619,10 +6750,6 @@ export function useAppLogic(props) {
|
|
|
6619
6750
|
}
|
|
6620
6751
|
if (!message.trim())
|
|
6621
6752
|
return;
|
|
6622
|
-
if (!hasApiKeyRef.current) {
|
|
6623
|
-
openApiKeyModal();
|
|
6624
|
-
return;
|
|
6625
|
-
}
|
|
6626
6753
|
// Council question response — route answer back to council generator.
|
|
6627
6754
|
// The card now owns keyboard input; this branch survives only for the
|
|
6628
6755
|
// legacy code path where the user typed an answer in the main prompt
|
|
@@ -6662,7 +6789,6 @@ export function useAppLogic(props) {
|
|
|
6662
6789
|
agent,
|
|
6663
6790
|
clearLiveTurnUi,
|
|
6664
6791
|
handleCommand,
|
|
6665
|
-
openApiKeyModal,
|
|
6666
6792
|
processMessage,
|
|
6667
6793
|
replacePasteBlocks,
|
|
6668
6794
|
scrollToBottom,
|
|
@@ -6672,6 +6798,8 @@ export function useAppLogic(props) {
|
|
|
6672
6798
|
setShowSlashMenuSync,
|
|
6673
6799
|
setPendingCouncilQuestionSync,
|
|
6674
6800
|
setCouncilCardStateSync,
|
|
6801
|
+
setShowModelPicker,
|
|
6802
|
+
setModelPickerIndex,
|
|
6675
6803
|
]);
|
|
6676
6804
|
// Switch to the "messages" branch (which renders log + halt-card + init-new-form +
|
|
6677
6805
|
// point-to-existing-form + council-progress) whenever ANY of these overlays
|
|
@@ -6709,7 +6837,8 @@ export function useAppLogic(props) {
|
|
|
6709
6837
|
apiKeyPrompt,
|
|
6710
6838
|
blockPrompt,
|
|
6711
6839
|
btwState,
|
|
6712
|
-
|
|
6840
|
+
oauthLogin,
|
|
6841
|
+
oauthProviders,
|
|
6713
6842
|
configuredProviders,
|
|
6714
6843
|
connectModalIndex,
|
|
6715
6844
|
contextStats,
|
|
@@ -6723,6 +6852,7 @@ export function useAppLogic(props) {
|
|
|
6723
6852
|
councilMessages,
|
|
6724
6853
|
councilTranscriptExpanded,
|
|
6725
6854
|
councilTodoExpanded,
|
|
6855
|
+
compactRun,
|
|
6726
6856
|
councilPhases,
|
|
6727
6857
|
councilPlaceholders,
|
|
6728
6858
|
councilProgress,
|