stagent 0.9.5 → 0.10.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/README.md +5 -42
- package/dist/cli.js +42 -18
- package/docs/.coverage-gaps.json +13 -55
- package/docs/.last-generated +1 -1
- package/docs/features/provider-runtimes.md +4 -0
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/tables.md +9 -2
- package/docs/features/workflows.md +10 -4
- package/docs/journeys/developer.md +15 -1
- package/docs/journeys/personal-use.md +21 -4
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +0 -21
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +6 -9
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
- package/src/components/chat/chat-command-popover.tsx +2 -2
- package/src/components/chat/chat-input.tsx +2 -3
- package/src/components/chat/chat-session-provider.tsx +720 -0
- package/src/components/chat/chat-shell.tsx +92 -401
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +4 -5
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +19 -0
- package/src/components/tasks/task-card.tsx +26 -3
- package/src/components/tasks/task-chip-bar.tsx +24 -0
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/claude-agent.ts +155 -18
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +29 -60
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/codex-engine.ts +16 -17
- package/src/lib/chat/context-builder.ts +5 -3
- package/src/lib/chat/engine.ts +50 -3
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/stagent-tools.ts +1 -0
- package/src/lib/chat/stream-telemetry.ts +132 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +26 -1
- package/src/lib/chat/tool-catalog.ts +2 -1
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +39 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +84 -20
- package/src/lib/chat/tools/workflow-tools.ts +234 -32
- package/src/lib/constants/settings.ts +8 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +20 -15
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +45 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +68 -23
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +78 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +153 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +232 -13
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/types.ts +10 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +207 -4
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -1,20 +1,17 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
4
|
-
import {
|
|
5
|
-
import type { ConversationRow, ChatMessageRow } from "@/lib/db/schema";
|
|
4
|
+
import type { ConversationRow } from "@/lib/db/schema";
|
|
6
5
|
import type { PromptCategory } from "@/lib/chat/types";
|
|
7
|
-
import {
|
|
8
|
-
import { usePersistedState } from "@/hooks/use-persisted-state";
|
|
6
|
+
import { useChatSession } from "./chat-session-provider";
|
|
9
7
|
import { ConversationList } from "./conversation-list";
|
|
10
8
|
import { ChatMessageList } from "./chat-message-list";
|
|
11
9
|
import { ChatInput } from "./chat-input";
|
|
12
|
-
import type { MentionReference } from "@/hooks/use-chat-autocomplete";
|
|
13
10
|
import { ChatEmptyState } from "./chat-empty-state";
|
|
14
11
|
import { ChatActivityIndicator } from "./chat-activity-indicator";
|
|
15
12
|
import { Button } from "@/components/ui/button";
|
|
16
13
|
import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet";
|
|
17
|
-
import {
|
|
14
|
+
import { PanelRightOpen } from "lucide-react";
|
|
18
15
|
|
|
19
16
|
interface ChatShellProps {
|
|
20
17
|
initialConversations: ConversationRow[];
|
|
@@ -22,60 +19,60 @@ interface ChatShellProps {
|
|
|
22
19
|
initialActiveId?: string | null;
|
|
23
20
|
}
|
|
24
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Thin view component for the /chat route. All chat-domain state lives in
|
|
24
|
+
* `ChatSessionProvider` (rendered from `src/app/layout.tsx`), so unmounting
|
|
25
|
+
* this component — e.g., when the user navigates to another sidebar view —
|
|
26
|
+
* does not touch the in-flight SSE reader loop or clear any messages. On
|
|
27
|
+
* remount, we read the provider's current state and render it directly.
|
|
28
|
+
*
|
|
29
|
+
* See `features/chat-session-persistence-provider.md`.
|
|
30
|
+
*/
|
|
25
31
|
export function ChatShell({
|
|
26
32
|
initialConversations,
|
|
27
33
|
promptCategories,
|
|
28
34
|
initialActiveId,
|
|
29
35
|
}: ChatShellProps) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
const session = useChatSession();
|
|
37
|
+
const {
|
|
38
|
+
conversations,
|
|
39
|
+
activeId,
|
|
40
|
+
messages,
|
|
41
|
+
isStreaming,
|
|
42
|
+
modelId,
|
|
43
|
+
availableModels,
|
|
44
|
+
hydrated,
|
|
45
|
+
hydrate,
|
|
46
|
+
setActiveConversation,
|
|
47
|
+
sendMessage,
|
|
48
|
+
stopStreaming,
|
|
49
|
+
createConversation,
|
|
50
|
+
deleteConversation,
|
|
51
|
+
renameConversation,
|
|
52
|
+
setMessageStatus,
|
|
53
|
+
setModelId,
|
|
54
|
+
} = session;
|
|
55
|
+
|
|
56
|
+
// View-local state only
|
|
38
57
|
const [mobileListOpen, setMobileListOpen] = useState(false);
|
|
39
58
|
const [hoverPreview, setHoverPreview] = useState<string | null>(null);
|
|
40
|
-
const [modelId, setModelId] = useState(DEFAULT_CHAT_MODEL);
|
|
41
|
-
const [availableModels, setAvailableModels] = useState<ChatModelOption[]>(CHAT_MODELS);
|
|
42
59
|
|
|
43
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const activeConversation = conversations.find((c) => c.id === activeId);
|
|
47
|
-
|
|
48
|
-
// Restore active conversation on mount
|
|
49
|
-
// Read localStorage synchronously to avoid race with usePersistedState's async useEffect
|
|
60
|
+
// Hydrate provider once with the server-rendered conversation list.
|
|
61
|
+
// Subsequent remounts are no-ops — the provider preserves its state.
|
|
50
62
|
useEffect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
if (restoredId && conversations.some((c) => c.id === restoredId)) {
|
|
58
|
-
setActiveId(restoredId);
|
|
59
|
-
setPersistedActiveId(restoredId);
|
|
60
|
-
// Fetch messages for restored conversation
|
|
61
|
-
fetch(`/api/chat/conversations/${restoredId}/messages`)
|
|
62
|
-
.then((r) => r.ok ? r.json() : [])
|
|
63
|
-
.then((msgs) => setMessages(msgs))
|
|
64
|
-
.catch(() => setMessages([]));
|
|
65
|
-
}
|
|
63
|
+
hydrate({
|
|
64
|
+
conversations: initialConversations,
|
|
65
|
+
initialActiveId: initialActiveId ?? null,
|
|
66
|
+
});
|
|
67
|
+
// Intentionally run only on mount: initialConversations is the
|
|
68
|
+
// server-rendered snapshot for this specific page visit.
|
|
66
69
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
67
70
|
}, []);
|
|
68
71
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (id) {
|
|
74
|
-
router.replace(`/chat?c=${id}`, { scroll: false });
|
|
75
|
-
} else {
|
|
76
|
-
router.replace("/chat", { scroll: false });
|
|
77
|
-
}
|
|
78
|
-
}, [router, setPersistedActiveId]);
|
|
72
|
+
const activeConversation = useMemo(
|
|
73
|
+
() => conversations.find((c) => c.id === activeId),
|
|
74
|
+
[conversations, activeId]
|
|
75
|
+
);
|
|
79
76
|
|
|
80
77
|
// Extract spawned task IDs from messages (execute_task tool results)
|
|
81
78
|
const spawnedTaskIds = useMemo(() => {
|
|
@@ -83,9 +80,14 @@ export function ChatShell({
|
|
|
83
80
|
for (const msg of messages) {
|
|
84
81
|
if (msg.metadata) {
|
|
85
82
|
try {
|
|
86
|
-
const meta =
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
const meta =
|
|
84
|
+
typeof msg.metadata === "string"
|
|
85
|
+
? JSON.parse(msg.metadata)
|
|
86
|
+
: msg.metadata;
|
|
87
|
+
if (
|
|
88
|
+
meta.type === "permission_request" &&
|
|
89
|
+
meta.toolName === "mcp__stagent__execute_task"
|
|
90
|
+
) {
|
|
89
91
|
const input = meta.toolInput;
|
|
90
92
|
if (input?.taskId) taskIds.push(input.taskId);
|
|
91
93
|
}
|
|
@@ -93,379 +95,63 @@ export function ChatShell({
|
|
|
93
95
|
// Ignore parse errors
|
|
94
96
|
}
|
|
95
97
|
}
|
|
96
|
-
// Also scan assistant message content for task execution confirmations
|
|
97
98
|
if (msg.role === "assistant" && msg.content) {
|
|
98
|
-
const taskIdMatch = msg.content.match(
|
|
99
|
+
const taskIdMatch = msg.content.match(
|
|
100
|
+
/Execution started.*?taskId["\s:]+([a-f0-9-]{36})/i
|
|
101
|
+
);
|
|
99
102
|
if (taskIdMatch) taskIds.push(taskIdMatch[1]);
|
|
100
103
|
}
|
|
101
104
|
}
|
|
102
105
|
return [...new Set(taskIds)];
|
|
103
106
|
}, [messages]);
|
|
104
107
|
|
|
105
|
-
//
|
|
106
|
-
useEffect(() => {
|
|
107
|
-
fetch("/api/settings/chat")
|
|
108
|
-
.then((r) => r.ok ? r.json() : null)
|
|
109
|
-
.then((data) => {
|
|
110
|
-
if (data?.defaultModel) setModelId(data.defaultModel);
|
|
111
|
-
})
|
|
112
|
-
.catch(() => {});
|
|
113
|
-
|
|
114
|
-
fetch("/api/chat/models")
|
|
115
|
-
.then((r) => r.ok ? r.json() : null)
|
|
116
|
-
.then((models) => {
|
|
117
|
-
if (models?.length) setAvailableModels(models);
|
|
118
|
-
})
|
|
119
|
-
.catch(() => {});
|
|
120
|
-
}, []);
|
|
121
|
-
|
|
122
|
-
// ── Conversation Management ──────────────────────────────────────────
|
|
123
|
-
|
|
108
|
+
// ── Action wrappers ──────────────────────────────────────────────────
|
|
124
109
|
const handleNewChat = useCallback(async () => {
|
|
125
|
-
|
|
126
|
-
const res = await fetch("/api/chat/conversations", {
|
|
127
|
-
method: "POST",
|
|
128
|
-
headers: { "Content-Type": "application/json" },
|
|
129
|
-
body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
|
|
130
|
-
});
|
|
131
|
-
if (!res.ok) return;
|
|
132
|
-
const conversation = await res.json();
|
|
133
|
-
setConversations((prev) => [conversation, ...prev]);
|
|
134
|
-
updateActiveId(conversation.id);
|
|
135
|
-
setMessages([]);
|
|
136
|
-
setMobileListOpen(false);
|
|
137
|
-
} catch {
|
|
138
|
-
// Handle error silently
|
|
139
|
-
}
|
|
140
|
-
}, [modelId, updateActiveId]);
|
|
141
|
-
|
|
142
|
-
const handleSelectConversation = useCallback(async (id: string) => {
|
|
143
|
-
updateActiveId(id);
|
|
110
|
+
await createConversation();
|
|
144
111
|
setMobileListOpen(false);
|
|
145
|
-
|
|
146
|
-
const [msgRes, convRes] = await Promise.all([
|
|
147
|
-
fetch(`/api/chat/conversations/${id}/messages`),
|
|
148
|
-
fetch(`/api/chat/conversations/${id}`),
|
|
149
|
-
]);
|
|
150
|
-
if (msgRes.ok) {
|
|
151
|
-
const msgs = await msgRes.json();
|
|
152
|
-
// Clean up stale "streaming" messages from interrupted sessions
|
|
153
|
-
setMessages(
|
|
154
|
-
msgs.map((m: ChatMessageRow) =>
|
|
155
|
-
m.status === "streaming" ? { ...m, status: "complete" as const } : m
|
|
156
|
-
)
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
if (convRes.ok) {
|
|
160
|
-
const conv = await convRes.json();
|
|
161
|
-
if (conv.modelId) setModelId(conv.modelId);
|
|
162
|
-
}
|
|
163
|
-
} catch {
|
|
164
|
-
setMessages([]);
|
|
165
|
-
}
|
|
166
|
-
}, [updateActiveId]);
|
|
112
|
+
}, [createConversation]);
|
|
167
113
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
method: "DELETE",
|
|
173
|
-
});
|
|
174
|
-
setConversations((prev) => prev.filter((c) => c.id !== id));
|
|
175
|
-
if (activeId === id) {
|
|
176
|
-
updateActiveId(null);
|
|
177
|
-
setMessages([]);
|
|
178
|
-
}
|
|
179
|
-
} catch {
|
|
180
|
-
// Handle error silently
|
|
181
|
-
}
|
|
114
|
+
const handleSelectConversation = useCallback(
|
|
115
|
+
(id: string) => {
|
|
116
|
+
setActiveConversation(id);
|
|
117
|
+
setMobileListOpen(false);
|
|
182
118
|
},
|
|
183
|
-
[
|
|
119
|
+
[setActiveConversation]
|
|
184
120
|
);
|
|
185
121
|
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
const res = await fetch(`/api/chat/conversations/${id}`, {
|
|
190
|
-
method: "PATCH",
|
|
191
|
-
headers: { "Content-Type": "application/json" },
|
|
192
|
-
body: JSON.stringify({ title }),
|
|
193
|
-
});
|
|
194
|
-
if (res.ok) {
|
|
195
|
-
const updated = await res.json();
|
|
196
|
-
setConversations((prev) =>
|
|
197
|
-
prev.map((c) => (c.id === id ? updated : c))
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
} catch {
|
|
201
|
-
// Handle error silently
|
|
202
|
-
}
|
|
203
|
-
},
|
|
204
|
-
[]
|
|
122
|
+
const handleDeleteConversation = useCallback(
|
|
123
|
+
(id: string) => deleteConversation(id),
|
|
124
|
+
[deleteConversation]
|
|
205
125
|
);
|
|
206
126
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
async (content: string, mentions?: MentionReference[]) => {
|
|
211
|
-
let conversationId = activeId;
|
|
212
|
-
|
|
213
|
-
// Create conversation on first message if none active
|
|
214
|
-
if (!conversationId) {
|
|
215
|
-
try {
|
|
216
|
-
const res = await fetch("/api/chat/conversations", {
|
|
217
|
-
method: "POST",
|
|
218
|
-
headers: { "Content-Type": "application/json" },
|
|
219
|
-
body: JSON.stringify({ runtimeId: getRuntimeForModel(modelId), modelId }),
|
|
220
|
-
});
|
|
221
|
-
if (!res.ok) return;
|
|
222
|
-
const conversation = await res.json();
|
|
223
|
-
setConversations((prev) => [conversation, ...prev]);
|
|
224
|
-
updateActiveId(conversation.id);
|
|
225
|
-
conversationId = conversation.id;
|
|
226
|
-
} catch {
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
// Add optimistic user message
|
|
232
|
-
const userMsg: ChatMessageRow = {
|
|
233
|
-
id: crypto.randomUUID(),
|
|
234
|
-
conversationId: conversationId!,
|
|
235
|
-
role: "user",
|
|
236
|
-
content,
|
|
237
|
-
metadata: null,
|
|
238
|
-
status: "complete",
|
|
239
|
-
createdAt: new Date(),
|
|
240
|
-
};
|
|
241
|
-
setMessages((prev) => [...prev, userMsg]);
|
|
242
|
-
|
|
243
|
-
// Add placeholder assistant message
|
|
244
|
-
const assistantMsgId = crypto.randomUUID();
|
|
245
|
-
const assistantMsg: ChatMessageRow = {
|
|
246
|
-
id: assistantMsgId,
|
|
247
|
-
conversationId: conversationId!,
|
|
248
|
-
role: "assistant",
|
|
249
|
-
content: "",
|
|
250
|
-
metadata: null,
|
|
251
|
-
status: "streaming",
|
|
252
|
-
createdAt: new Date(),
|
|
253
|
-
};
|
|
254
|
-
setMessages((prev) => [...prev, assistantMsg]);
|
|
255
|
-
|
|
256
|
-
setIsStreaming(true);
|
|
257
|
-
const controller = new AbortController();
|
|
258
|
-
setAbortController(controller);
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
const res = await fetch(
|
|
262
|
-
`/api/chat/conversations/${conversationId}/messages`,
|
|
263
|
-
{
|
|
264
|
-
method: "POST",
|
|
265
|
-
headers: { "Content-Type": "application/json" },
|
|
266
|
-
body: JSON.stringify({ content, mentions }),
|
|
267
|
-
signal: controller.signal,
|
|
268
|
-
}
|
|
269
|
-
);
|
|
270
|
-
|
|
271
|
-
if (!res.ok || !res.body) {
|
|
272
|
-
throw new Error("Failed to send message");
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const reader = res.body.getReader();
|
|
276
|
-
const decoder = new TextDecoder();
|
|
277
|
-
let buffer = "";
|
|
278
|
-
|
|
279
|
-
while (true) {
|
|
280
|
-
const { done, value } = await reader.read();
|
|
281
|
-
if (done) break;
|
|
282
|
-
|
|
283
|
-
buffer += decoder.decode(value, { stream: true });
|
|
284
|
-
const lines = buffer.split("\n");
|
|
285
|
-
buffer = lines.pop() ?? "";
|
|
286
|
-
|
|
287
|
-
for (const line of lines) {
|
|
288
|
-
if (!line.startsWith("data: ")) continue;
|
|
289
|
-
const json = line.slice(6);
|
|
290
|
-
try {
|
|
291
|
-
const event = JSON.parse(json);
|
|
292
|
-
if (event.type === "status") {
|
|
293
|
-
setMessages((prev) =>
|
|
294
|
-
prev.map((m) =>
|
|
295
|
-
m.id === assistantMsgId
|
|
296
|
-
? { ...m, metadata: JSON.stringify({ statusPhase: event.phase, statusMessage: event.message }) }
|
|
297
|
-
: m
|
|
298
|
-
)
|
|
299
|
-
);
|
|
300
|
-
} else if (event.type === "delta") {
|
|
301
|
-
setMessages((prev) =>
|
|
302
|
-
prev.map((m) =>
|
|
303
|
-
m.id === assistantMsgId
|
|
304
|
-
? { ...m, content: m.content + event.content }
|
|
305
|
-
: m
|
|
306
|
-
)
|
|
307
|
-
);
|
|
308
|
-
} else if (event.type === "done") {
|
|
309
|
-
setMessages((prev) =>
|
|
310
|
-
prev.map((m) =>
|
|
311
|
-
m.id === assistantMsgId
|
|
312
|
-
? {
|
|
313
|
-
...m,
|
|
314
|
-
id: event.messageId,
|
|
315
|
-
status: "complete",
|
|
316
|
-
metadata: (() => {
|
|
317
|
-
const existing = m.metadata
|
|
318
|
-
? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })()
|
|
319
|
-
: {};
|
|
320
|
-
if (event.quickAccess?.length) {
|
|
321
|
-
existing.quickAccess = event.quickAccess;
|
|
322
|
-
}
|
|
323
|
-
return JSON.stringify(existing);
|
|
324
|
-
})(),
|
|
325
|
-
}
|
|
326
|
-
: m
|
|
327
|
-
)
|
|
328
|
-
);
|
|
329
|
-
// Refresh conversation from API to get auto-generated title
|
|
330
|
-
fetch(`/api/chat/conversations/${conversationId}`)
|
|
331
|
-
.then((r) => r.ok ? r.json() : null)
|
|
332
|
-
.then((conv) => {
|
|
333
|
-
if (conv) {
|
|
334
|
-
setConversations((prev) =>
|
|
335
|
-
prev.map((c) =>
|
|
336
|
-
c.id === conversationId
|
|
337
|
-
? { ...c, title: conv.title, updatedAt: new Date() }
|
|
338
|
-
: c
|
|
339
|
-
)
|
|
340
|
-
);
|
|
341
|
-
}
|
|
342
|
-
})
|
|
343
|
-
.catch(() => {});
|
|
344
|
-
} else if (event.type === "permission_request" || event.type === "question") {
|
|
345
|
-
// Insert system message for inline permission/question UI
|
|
346
|
-
const systemMsg = {
|
|
347
|
-
id: event.messageId,
|
|
348
|
-
conversationId: conversationId!,
|
|
349
|
-
role: "system" as const,
|
|
350
|
-
content: event.type === "permission_request"
|
|
351
|
-
? `Permission required: ${event.toolName}`
|
|
352
|
-
: "Agent has a question",
|
|
353
|
-
metadata: JSON.stringify(event.type === "permission_request"
|
|
354
|
-
? { type: "permission_request", requestId: event.requestId, toolName: event.toolName, toolInput: event.toolInput }
|
|
355
|
-
: { type: "question", requestId: event.requestId, questions: event.questions }
|
|
356
|
-
),
|
|
357
|
-
status: "pending" as const,
|
|
358
|
-
createdAt: new Date(),
|
|
359
|
-
};
|
|
360
|
-
setMessages((prev) => [...prev, systemMsg]);
|
|
361
|
-
} else if (event.type === "screenshot") {
|
|
362
|
-
// Append screenshot attachment to assistant message metadata
|
|
363
|
-
setMessages((prev) =>
|
|
364
|
-
prev.map((m) => {
|
|
365
|
-
if (m.id !== assistantMsgId) return m;
|
|
366
|
-
const meta = m.metadata ? (() => { try { return JSON.parse(m.metadata!); } catch { return {}; } })() : {};
|
|
367
|
-
const attachments = Array.isArray(meta.attachments) ? meta.attachments : [];
|
|
368
|
-
attachments.push({
|
|
369
|
-
documentId: event.documentId,
|
|
370
|
-
thumbnailUrl: event.thumbnailUrl,
|
|
371
|
-
originalUrl: event.originalUrl,
|
|
372
|
-
width: event.width,
|
|
373
|
-
height: event.height,
|
|
374
|
-
});
|
|
375
|
-
return { ...m, metadata: JSON.stringify({ ...meta, attachments }) };
|
|
376
|
-
})
|
|
377
|
-
);
|
|
378
|
-
} else if (event.type === "error") {
|
|
379
|
-
setMessages((prev) =>
|
|
380
|
-
prev.map((m) =>
|
|
381
|
-
m.id === assistantMsgId
|
|
382
|
-
? {
|
|
383
|
-
...m,
|
|
384
|
-
content: m.content || event.message,
|
|
385
|
-
status: "error",
|
|
386
|
-
}
|
|
387
|
-
: m
|
|
388
|
-
)
|
|
389
|
-
);
|
|
390
|
-
}
|
|
391
|
-
} catch {
|
|
392
|
-
// Ignore malformed SSE data
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
} catch (error) {
|
|
397
|
-
if ((error as Error).name !== "AbortError") {
|
|
398
|
-
setMessages((prev) =>
|
|
399
|
-
prev.map((m) =>
|
|
400
|
-
m.id === assistantMsgId
|
|
401
|
-
? {
|
|
402
|
-
...m,
|
|
403
|
-
content:
|
|
404
|
-
m.content || "Failed to get response. Please try again.",
|
|
405
|
-
status: "error",
|
|
406
|
-
}
|
|
407
|
-
: m
|
|
408
|
-
)
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
} finally {
|
|
412
|
-
setIsStreaming(false);
|
|
413
|
-
setAbortController(null);
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
[activeId, modelId, updateActiveId]
|
|
127
|
+
const handleRenameConversation = useCallback(
|
|
128
|
+
(id: string, title: string) => renameConversation(id, title),
|
|
129
|
+
[renameConversation]
|
|
417
130
|
);
|
|
418
131
|
|
|
419
|
-
const handleStop = useCallback(() => {
|
|
420
|
-
abortController?.abort();
|
|
421
|
-
}, [abortController]);
|
|
422
|
-
|
|
423
132
|
const handleSuggestionClick = useCallback(
|
|
424
133
|
(prompt: string) => {
|
|
425
|
-
|
|
134
|
+
void sendMessage(prompt);
|
|
426
135
|
},
|
|
427
|
-
[
|
|
136
|
+
[sendMessage]
|
|
428
137
|
);
|
|
429
138
|
|
|
430
139
|
const handleMessageStatusChange = useCallback(
|
|
431
140
|
(messageId: string, status: string) => {
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
? { ...m, status: status as "pending" | "streaming" | "complete" | "error" }
|
|
436
|
-
: m
|
|
437
|
-
)
|
|
141
|
+
setMessageStatus(
|
|
142
|
+
messageId,
|
|
143
|
+
status as "pending" | "streaming" | "complete" | "error"
|
|
438
144
|
);
|
|
439
145
|
},
|
|
440
|
-
[]
|
|
146
|
+
[setMessageStatus]
|
|
441
147
|
);
|
|
442
148
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
if (activeId) {
|
|
448
|
-
const newRuntimeId = getRuntimeForModel(newModelId);
|
|
449
|
-
await fetch(`/api/chat/conversations/${activeId}`, {
|
|
450
|
-
method: "PATCH",
|
|
451
|
-
headers: { "Content-Type": "application/json" },
|
|
452
|
-
body: JSON.stringify({ modelId: newModelId, runtimeId: newRuntimeId }),
|
|
453
|
-
}).catch(() => {});
|
|
454
|
-
// Update local state so conversation list reflects the change
|
|
455
|
-
setConversations((prev) =>
|
|
456
|
-
prev.map((c) =>
|
|
457
|
-
c.id === activeId
|
|
458
|
-
? { ...c, modelId: newModelId, runtimeId: newRuntimeId }
|
|
459
|
-
: c
|
|
460
|
-
)
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
},
|
|
464
|
-
[activeId]
|
|
465
|
-
);
|
|
149
|
+
// Suppress unused warnings from props we still accept but no longer own.
|
|
150
|
+
// `hydrated` tells us whether the provider has data — we can use it to
|
|
151
|
+
// skip the empty-state flash on a remount that finds existing state.
|
|
152
|
+
void hydrated;
|
|
466
153
|
|
|
467
154
|
// ── Render ───────────────────────────────────────────────────────────
|
|
468
|
-
|
|
469
155
|
const conversationListContent = (
|
|
470
156
|
<ConversationList
|
|
471
157
|
conversations={conversations}
|
|
@@ -507,13 +193,13 @@ export function ChatShell({
|
|
|
507
193
|
onHoverPreview={setHoverPreview}
|
|
508
194
|
>
|
|
509
195
|
<ChatInput
|
|
510
|
-
onSend={
|
|
511
|
-
onStop={
|
|
196
|
+
onSend={sendMessage}
|
|
197
|
+
onStop={stopStreaming}
|
|
512
198
|
isStreaming={isStreaming}
|
|
513
199
|
isHeroMode
|
|
514
200
|
previewText={hoverPreview}
|
|
515
201
|
modelId={modelId}
|
|
516
|
-
onModelChange={
|
|
202
|
+
onModelChange={setModelId}
|
|
517
203
|
availableModels={availableModels}
|
|
518
204
|
projectId={activeConversation?.projectId}
|
|
519
205
|
/>
|
|
@@ -523,7 +209,12 @@ export function ChatShell({
|
|
|
523
209
|
<>
|
|
524
210
|
{/* Messages */}
|
|
525
211
|
<div className="flex-1 overflow-hidden">
|
|
526
|
-
<ChatMessageList
|
|
212
|
+
<ChatMessageList
|
|
213
|
+
messages={messages}
|
|
214
|
+
isStreaming={isStreaming}
|
|
215
|
+
conversationId={activeId ?? undefined}
|
|
216
|
+
onMessageStatusChange={handleMessageStatusChange}
|
|
217
|
+
/>
|
|
527
218
|
</div>
|
|
528
219
|
|
|
529
220
|
{/* Background activity indicator */}
|
|
@@ -533,12 +224,12 @@ export function ChatShell({
|
|
|
533
224
|
|
|
534
225
|
{/* Docked input */}
|
|
535
226
|
<ChatInput
|
|
536
|
-
onSend={
|
|
537
|
-
onStop={
|
|
227
|
+
onSend={sendMessage}
|
|
228
|
+
onStop={stopStreaming}
|
|
538
229
|
isStreaming={isStreaming}
|
|
539
230
|
isHeroMode={false}
|
|
540
231
|
modelId={modelId}
|
|
541
|
-
onModelChange={
|
|
232
|
+
onModelChange={setModelId}
|
|
542
233
|
availableModels={availableModels}
|
|
543
234
|
projectId={activeConversation?.projectId}
|
|
544
235
|
/>
|