@nextclaw/ui 0.14.0 → 0.14.1-beta.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.
Files changed (130) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/assets/{api-B50yVdT8.js → api-C6EDHBDR.js} +1 -1
  3. package/dist/assets/channels-list-page-DKj6m1Hc.js +8 -0
  4. package/dist/assets/chat-page-BXngLJY6.js +105 -0
  5. package/dist/assets/config-split-page-CjlmJy8X.js +1 -0
  6. package/dist/assets/{confirm-dialog-DRSelLdI.js → confirm-dialog-B39X0pYp.js} +1 -1
  7. package/dist/assets/desktop-update-config-DfUQ8nly.js +1 -0
  8. package/dist/assets/{dist-NZRV-BxD.js → dist-BIYV2-Ia.js} +1 -1
  9. package/dist/assets/{dist-D19wiQwB.js → dist-CmMro3nV.js} +1 -1
  10. package/dist/assets/doc-browser-B6Wc_TIY.js +1 -0
  11. package/dist/assets/doc-browser-CDtUtcim.js +1 -0
  12. package/dist/assets/{doc-browser-context-C8KPbj-p.js → doc-browser-context-DZbC9zVB.js} +1 -1
  13. package/dist/assets/{ellipsis-BYy8TP0A.js → ellipsis-DDpgTzgx.js} +1 -1
  14. package/dist/assets/{external-link-528k6MDD.js → external-link-D2cBOCqn.js} +1 -1
  15. package/dist/assets/index-DTb_XVIH.js +103 -0
  16. package/dist/assets/index-ayiPYhf1.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-DZ9H0v05.js +1 -0
  18. package/dist/assets/mcp-marketplace-page-cnVWf7QQ.js +40 -0
  19. package/dist/assets/model-config-page-Bcb35AVv.js +1 -0
  20. package/dist/assets/plus-CYE1onU2.js +1 -0
  21. package/dist/assets/provider-scoped-model-input-kdoNp4HG.js +1 -0
  22. package/dist/assets/providers-config-page-CQSibSwO.js +1 -0
  23. package/dist/assets/{react-Dsotw5vM.js → react-DYQXXGbH.js} +1 -1
  24. package/dist/assets/remote-BlJDbXOI.js +1 -0
  25. package/dist/assets/rotate-cw-BWbvFDAX.js +1 -0
  26. package/dist/assets/runtime-config-page-Tn2yLQ0c.js +1 -0
  27. package/dist/assets/{save-CHDnDTok.js → save-BNVTgBVn.js} +1 -1
  28. package/dist/assets/search-CKCzwLId.js +1 -0
  29. package/dist/assets/search-config-page-DG5Q2hF1.js +1 -0
  30. package/dist/assets/{secrets-config-page-95yaH1fa.js → secrets-config-page-BXn5Uuwp.js} +2 -2
  31. package/dist/assets/select-CIRwTNP4.js +41 -0
  32. package/dist/assets/{tag-chip-Cr8GwpRE.js → tag-chip-DKhVFtTE.js} +1 -1
  33. package/dist/assets/{use-config-TzwzTgg0.js → use-config-DgKM9VIU.js} +1 -1
  34. package/dist/assets/x-Cm3JjYut.js +1 -0
  35. package/dist/index.html +14 -27
  36. package/package.json +9 -9
  37. package/src/app/presenters/app.presenter.ts +2 -0
  38. package/src/features/account/components/account-panel.tsx +2 -2
  39. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -0
  40. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +39 -80
  41. package/src/features/chat/features/input/hooks/use-chat-input-bar-query-state.ts +118 -0
  42. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +2 -17
  43. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -24
  44. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +0 -27
  45. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +89 -0
  46. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +9 -1
  47. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +115 -0
  48. package/src/features/chat/features/message/components/chat-message-list.container.tsx +4 -0
  49. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +119 -1
  50. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +51 -0
  51. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +28 -5
  52. package/src/features/chat/features/message/utils/chat-message-tool-card.utils.ts +4 -0
  53. package/src/features/chat/features/ncp/hooks/__tests__/use-ui-show-content-event.test.tsx +1 -0
  54. package/src/features/chat/features/ncp/utils/ncp-chat-query-derived.utils.ts +11 -3
  55. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +27 -0
  56. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +19 -1
  57. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +46 -0
  58. package/src/features/chat/features/session/hooks/__tests__/use-selected-session-context-window-indicator.test.tsx +63 -0
  59. package/src/features/chat/features/session/hooks/use-selected-session-context-window-indicator.ts +7 -10
  60. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +8 -0
  61. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +2 -0
  62. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +1 -0
  63. package/src/features/chat/features/session-type/utils/chat-session-type.utils.ts +4 -0
  64. package/src/features/chat/index.ts +1 -0
  65. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +28 -0
  66. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +129 -0
  67. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +34 -0
  68. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +143 -1
  69. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +30 -0
  70. package/src/features/chat/managers/__tests__/recent-selection.manager.test.ts +37 -14
  71. package/src/features/chat/managers/chat-draft-intent.manager.ts +46 -0
  72. package/src/features/chat/managers/chat-input.manager.ts +13 -13
  73. package/src/features/chat/managers/chat-run.manager.ts +3 -0
  74. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +19 -2
  75. package/src/features/chat/managers/chat-thread.manager.ts +4 -0
  76. package/src/features/chat/managers/chat-ui.manager.ts +2 -6
  77. package/src/features/chat/managers/recent-selection.manager.ts +21 -67
  78. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +47 -1
  79. package/src/features/chat/pages/ncp-chat-page.tsx +18 -0
  80. package/src/features/chat/stores/chat-input.store.ts +7 -1
  81. package/src/features/chat/stores/recent-selection.store.ts +85 -0
  82. package/src/features/chat/types/chat-input-bar.types.ts +0 -1
  83. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +137 -0
  84. package/src/features/panel-apps/components/panel-apps-list.tsx +119 -5
  85. package/src/features/panel-apps/index.ts +2 -0
  86. package/src/features/panel-apps/utils/panel-app-entry-match.utils.ts +17 -0
  87. package/src/shared/lib/api/chat-session-type.types.ts +7 -0
  88. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  89. package/src/shared/lib/api/types.ts +3 -0
  90. package/src/shared/lib/i18n/locales/en-US/chat.json +4 -0
  91. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +10 -0
  92. package/src/shared/lib/i18n/locales/zh-CN/chat.json +4 -0
  93. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +10 -0
  94. package/dist/assets/app-presenter-provider-BI-ewLbO.js +0 -3
  95. package/dist/assets/arrow-left-BOCJyLbQ.js +0 -1
  96. package/dist/assets/book-open-Bd2Xbrrc.js +0 -1
  97. package/dist/assets/channels-list-page-moJkpxtU.js +0 -8
  98. package/dist/assets/chat-page-CO27cl4v.js +0 -105
  99. package/dist/assets/config-split-page-CsQ2rIdq.js +0 -1
  100. package/dist/assets/desktop-update-config-qa-SynOc.js +0 -1
  101. package/dist/assets/doc-browser-2ddUJCA0.js +0 -1
  102. package/dist/assets/doc-browser-BjHUczQb.js +0 -1
  103. package/dist/assets/es2015-BUIIKCNh.js +0 -41
  104. package/dist/assets/host-capabilities-9bB_998Z.js +0 -1
  105. package/dist/assets/index-CMNrRhnG.js +0 -101
  106. package/dist/assets/index-UTmTW1eU.css +0 -1
  107. package/dist/assets/key-round-D3vD9dj7.js +0 -1
  108. package/dist/assets/loader-circle-DV9TSRns.js +0 -1
  109. package/dist/assets/mcp-marketplace-page-CS5Lh3oA.js +0 -1
  110. package/dist/assets/mcp-marketplace-page-DEOMcWb0.js +0 -40
  111. package/dist/assets/model-config-page-BSc4o7pb.js +0 -1
  112. package/dist/assets/notice-card-qqlbsOoR.js +0 -1
  113. package/dist/assets/plus-Bf9qhPDO.js +0 -1
  114. package/dist/assets/popover-BnmPOXe3.js +0 -1
  115. package/dist/assets/provider-scoped-model-input-Ca56s475.js +0 -1
  116. package/dist/assets/providers-config-page-D8Kf4U3u.js +0 -1
  117. package/dist/assets/refresh-cw-Bmx4wc9P.js +0 -1
  118. package/dist/assets/remote-DNMEy_c4.js +0 -1
  119. package/dist/assets/rotate-cw-CL0sVBel.js +0 -1
  120. package/dist/assets/runtime-config-page-CUFtKr0U.js +0 -1
  121. package/dist/assets/search-D7L-TCH-.js +0 -1
  122. package/dist/assets/search-config-page-CA3RVAef.js +0 -1
  123. package/dist/assets/status-dot-Dm4h-bjn.js +0 -1
  124. package/dist/assets/tabs-CJvw5Lqp.js +0 -1
  125. package/dist/assets/tooltip-Bxzi6aDL.js +0 -1
  126. package/dist/assets/trash-2-CSfX-BvU.js +0 -1
  127. package/dist/assets/x-fkIWvi07.js +0 -1
  128. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-bar-controller.test.tsx +0 -152
  129. package/src/features/chat/features/input/hooks/use-chat-input-bar-controller.ts +0 -142
  130. /package/dist/assets/{config-hints-Cl2XAhzV.js → config-hints-CTqBnCDp.js} +0 -0
@@ -42,6 +42,12 @@ function readPurpose(value: unknown): ChatUiShowContentRequest["purpose"] {
42
42
  : undefined;
43
43
  }
44
44
 
45
+ function readPlacement(value: unknown): ChatUiShowContentRequest["placement"] {
46
+ return value === "inline" || value === "side_panel"
47
+ ? value
48
+ : undefined;
49
+ }
50
+
45
51
  function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null {
46
52
  if (!isRecord(value) || !isRecord(value.target)) {
47
53
  return null;
@@ -57,6 +63,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
57
63
  }
58
64
  const title = readOptionalString(value.title);
59
65
  const purpose = readPurpose(value.purpose);
66
+ const placement = readPlacement(value.placement);
60
67
 
61
68
  if (targetType === "file") {
62
69
  const path = readOptionalString(payload.path);
@@ -74,6 +81,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
74
81
  },
75
82
  title,
76
83
  purpose,
84
+ placement,
77
85
  };
78
86
  }
79
87
 
@@ -91,6 +99,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
91
99
  },
92
100
  title,
93
101
  purpose,
102
+ placement,
94
103
  };
95
104
  }
96
105
 
@@ -107,6 +116,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
107
116
  },
108
117
  title,
109
118
  purpose,
119
+ placement,
110
120
  };
111
121
  }
112
122
 
@@ -147,6 +157,22 @@ export function buildShowContentToolCard(params: {
147
157
  if (!request) {
148
158
  return null;
149
159
  }
160
+ const sidePanelRequest: ChatUiShowContentRequest = {
161
+ ...request,
162
+ placement: "side_panel",
163
+ };
164
+ const showContentAction = {
165
+ kind: "show-content" as const,
166
+ label: actionLabel,
167
+ request: sidePanelRequest,
168
+ };
169
+ const panelApp = request.target.type === "panel_app" && request.placement === "inline"
170
+ ? {
171
+ appId: request.target.payload.appId,
172
+ title: request.title,
173
+ action: showContentAction,
174
+ }
175
+ : undefined;
150
176
  return {
151
177
  kind: "result",
152
178
  name: SHOW_CONTENT_TOOL_NAME,
@@ -156,10 +182,7 @@ export function buildShowContentToolCard(params: {
156
182
  hasResult: true,
157
183
  statusTone: "success",
158
184
  statusLabel,
159
- action: {
160
- kind: "show-content",
161
- label: actionLabel,
162
- request,
163
- },
185
+ action: showContentAction,
186
+ ...(panelApp ? { panelApp } : {}),
164
187
  };
165
188
  }
@@ -15,6 +15,7 @@ export type ToolCardViewSource = ToolCard & {
15
15
  action?: ChatToolPartViewModel["action"];
16
16
  fileOperation?: ChatToolPartViewModel["fileOperation"];
17
17
  outputData?: unknown;
18
+ panelApp?: ChatToolPartViewModel["panelApp"];
18
19
  };
19
20
 
20
21
  function isRecord(value: unknown): value is Record<string, unknown> {
@@ -133,6 +134,9 @@ export function buildToolCard(
133
134
  ...("fileOperation" in toolCard && toolCard.fileOperation
134
135
  ? { fileOperation: toolCard.fileOperation }
135
136
  : {}),
137
+ ...("panelApp" in toolCard && toolCard.panelApp
138
+ ? { panelApp: toolCard.panelApp }
139
+ : {}),
136
140
  };
137
141
  }
138
142
 
@@ -32,6 +32,7 @@ describe('useUiShowContentEvent', () => {
32
32
  },
33
33
  title: 'Example URL',
34
34
  purpose: 'read' as const,
35
+ placement: undefined,
35
36
  };
36
37
 
37
38
  const rendered = render(
@@ -6,13 +6,16 @@ import type { ConfigView, ProvidersView, ProviderTemplatesView } from '@/shared/
6
6
  import type { ChatModelOption } from '@/features/chat/types/chat-input.types';
7
7
  import { buildProviderModelCatalog, composeProviderModel, resolveModelThinkingCapability } from '@/shared/lib/provider-models';
8
8
 
9
- function buildRuntimeDefaultModelOption(label: string): ChatModelOption {
9
+ function buildRuntimeDefaultModelOption(
10
+ label: string,
11
+ thinkingCapability?: ChatModelOption['thinkingCapability'],
12
+ ): ChatModelOption {
10
13
  return {
11
14
  value: RUNTIME_DEFAULT_MODEL_VALUE,
12
15
  modelLabel: label,
13
16
  providerLabel: '',
14
17
  isRuntimeDefault: true,
15
- thinkingCapability: null,
18
+ thinkingCapability: thinkingCapability ?? null,
16
19
  };
17
20
  }
18
21
 
@@ -50,16 +53,21 @@ export function buildNcpChatProviderModelOptions(params: {
50
53
  export function filterNcpChatModelOptionsBySessionType(params: {
51
54
  modelOptions: ChatModelOption[];
52
55
  modelSelectionMode?: RuntimeModelSelectionMode;
56
+ runtimeDefaultThinkingCapability?: ChatModelOption['thinkingCapability'];
53
57
  runtimeDefaultModelLabel?: string;
54
58
  supportedModels?: string[];
55
59
  }): ChatModelOption[] {
56
60
  const {
57
61
  modelOptions,
58
62
  modelSelectionMode,
63
+ runtimeDefaultThinkingCapability,
59
64
  runtimeDefaultModelLabel = 'Runtime default',
60
65
  supportedModels,
61
66
  } = params;
62
- const runtimeDefaultOption = buildRuntimeDefaultModelOption(runtimeDefaultModelLabel);
67
+ const runtimeDefaultOption = buildRuntimeDefaultModelOption(
68
+ runtimeDefaultModelLabel,
69
+ runtimeDefaultThinkingCapability,
70
+ );
63
71
  if (modelSelectionMode === 'runtime-default') {
64
72
  return [runtimeDefaultOption];
65
73
  }
@@ -45,6 +45,7 @@ describe('ChatSessionHeaderActions', () => {
45
45
 
46
46
  expect(screen.getByText('Set Project Directory')).toBeTruthy();
47
47
  expect(screen.queryByText('Clear Project Directory')).toBeNull();
48
+ expect(screen.getByText('View Metadata')).toBeTruthy();
48
49
  expect(screen.getByText('Delete Session')).toBeTruthy();
49
50
  });
50
51
 
@@ -67,6 +68,32 @@ describe('ChatSessionHeaderActions', () => {
67
68
 
68
69
  expect(screen.getByText('Set Project Directory')).toBeTruthy();
69
70
  expect(screen.queryByText('Clear Project Directory')).toBeNull();
71
+ expect(screen.getByText('View Metadata')).toBeTruthy();
72
+ });
73
+
74
+ it('opens the session metadata dialog from the more-actions menu', async () => {
75
+ const user = userEvent.setup();
76
+
77
+ render(
78
+ <ChatSessionHeaderActions
79
+ sessionKey="session-codex"
80
+ canDeleteSession
81
+ isDeletePending={false}
82
+ projectRoot={null}
83
+ metadata={{
84
+ codex_thread_id: 'thread-123',
85
+ runtime: 'codex',
86
+ }}
87
+ onDeleteSession={mocks.onDeleteSession}
88
+ />
89
+ );
90
+
91
+ await user.click(screen.getByRole('button', { name: 'More actions' }));
92
+ await user.click(screen.getByRole('button', { name: 'View Metadata' }));
93
+
94
+ expect(screen.getByRole('dialog', { name: 'Session Metadata' })).toBeTruthy();
95
+ expect(screen.getByText(/codex_thread_id/)).toBeTruthy();
96
+ expect(screen.getByText(/thread-123/)).toBeTruthy();
70
97
  });
71
98
 
72
99
  it('shows a dedicated child-session entry button when the current session has child sessions', async () => {
@@ -1,9 +1,10 @@
1
1
  import { useState } from 'react';
2
- import { AlarmClock, FolderOpen, GitBranch, MoreVertical, Trash2 } from 'lucide-react';
2
+ import { AlarmClock, Braces, FolderOpen, GitBranch, MoreVertical, Trash2 } from 'lucide-react';
3
3
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { useChatSessionProject } from '@/features/chat/features/session/hooks/use-chat-session-project';
6
6
  import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
7
+ import { ChatSessionMetadataDialog } from './chat-session-metadata-dialog';
7
8
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
8
9
  import { t } from '@/shared/lib/i18n';
9
10
 
@@ -15,6 +16,7 @@ type ChatSessionHeaderActionsProps = {
15
16
  canDeleteSession: boolean;
16
17
  isDeletePending: boolean;
17
18
  projectRoot?: string | null;
19
+ metadata?: Record<string, unknown> | null;
18
20
  childSessionCount?: number;
19
21
  sessionCronJobCount?: number;
20
22
  onOpenChildSessions?: () => void;
@@ -27,6 +29,7 @@ export function ChatSessionHeaderActions({
27
29
  canDeleteSession,
28
30
  isDeletePending,
29
31
  projectRoot,
32
+ metadata,
30
33
  childSessionCount = 0,
31
34
  sessionCronJobCount = 0,
32
35
  onOpenChildSessions,
@@ -36,6 +39,7 @@ export function ChatSessionHeaderActions({
36
39
  const updateSessionProject = useChatSessionProject();
37
40
  const [isMenuOpen, setIsMenuOpen] = useState(false);
38
41
  const [isDialogOpen, setIsDialogOpen] = useState(false);
42
+ const [isMetadataDialogOpen, setIsMetadataDialogOpen] = useState(false);
39
43
  const [isProjectPending, setIsProjectPending] = useState(false);
40
44
  const isBusy = isDeletePending || isProjectPending;
41
45
 
@@ -96,6 +100,15 @@ export function ChatSessionHeaderActions({
96
100
  }}
97
101
  disabled={isBusy}
98
102
  />
103
+ <ChatSessionHeaderMenuItem
104
+ icon={Braces}
105
+ label={t('chatSessionViewMetadata')}
106
+ onClick={() => {
107
+ setIsMenuOpen(false);
108
+ setIsMetadataDialogOpen(true);
109
+ }}
110
+ disabled={isBusy}
111
+ />
99
112
  <ChatSessionHeaderMenuItem
100
113
  icon={Trash2}
101
114
  label={t('chatDeleteSession')}
@@ -117,6 +130,11 @@ export function ChatSessionHeaderActions({
117
130
  onOpenChange={setIsDialogOpen}
118
131
  onSave={runProjectUpdate}
119
132
  />
133
+ <ChatSessionMetadataDialog
134
+ open={isMetadataDialogOpen}
135
+ metadata={metadata}
136
+ onOpenChange={setIsMetadataDialogOpen}
137
+ />
120
138
  </div>
121
139
  );
122
140
  }
@@ -0,0 +1,46 @@
1
+ import {
2
+ Dialog,
3
+ DialogContent,
4
+ DialogDescription,
5
+ DialogHeader,
6
+ DialogTitle,
7
+ } from '@/shared/components/ui/dialog';
8
+ import { t } from '@/shared/lib/i18n';
9
+
10
+ type ChatSessionMetadataDialogProps = {
11
+ open: boolean;
12
+ metadata?: Record<string, unknown> | null;
13
+ onOpenChange: (open: boolean) => void;
14
+ };
15
+
16
+ function formatSessionMetadata(metadata?: Record<string, unknown> | null): string {
17
+ try {
18
+ return JSON.stringify(metadata ?? {}, null, 2);
19
+ } catch {
20
+ return '{}';
21
+ }
22
+ }
23
+
24
+ export function ChatSessionMetadataDialog({
25
+ open,
26
+ metadata,
27
+ onOpenChange,
28
+ }: ChatSessionMetadataDialogProps) {
29
+ return (
30
+ <Dialog open={open} onOpenChange={onOpenChange}>
31
+ <DialogContent className="max-w-2xl gap-0 overflow-hidden p-0">
32
+ <DialogHeader className="border-b border-gray-200 px-5 py-4 pr-12">
33
+ <DialogTitle className="text-base">{t('chatSessionMetadataDialogTitle')}</DialogTitle>
34
+ <DialogDescription className="sr-only">
35
+ {t('chatSessionMetadataDialogDescription')}
36
+ </DialogDescription>
37
+ </DialogHeader>
38
+ <div className="px-5 pb-5 pt-4">
39
+ <pre className="max-h-[60vh] overflow-auto rounded-lg border border-gray-200 bg-gray-950 p-4 text-xs leading-relaxed text-gray-100">
40
+ {formatSessionMetadata(metadata)}
41
+ </pre>
42
+ </div>
43
+ </DialogContent>
44
+ </Dialog>
45
+ );
46
+ }
@@ -0,0 +1,63 @@
1
+ import { renderHook } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { useSelectedSessionContextWindowIndicator } from '@/features/chat/features/session/hooks/use-selected-session-context-window-indicator';
4
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
5
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
6
+
7
+ describe('useSelectedSessionContextWindowIndicator', () => {
8
+ beforeEach(() => {
9
+ useChatSessionListStore.getState().setSnapshot({
10
+ selectedSessionKey: null,
11
+ });
12
+ useChatThreadStore.getState().setSnapshot({
13
+ sessionKey: null,
14
+ contextWindow: null,
15
+ });
16
+ });
17
+
18
+ it('shows the current thread context window once the thread snapshot has it', () => {
19
+ useChatSessionListStore.getState().setSnapshot({
20
+ selectedSessionKey: 'session-other',
21
+ });
22
+ useChatThreadStore.getState().setSnapshot({
23
+ sessionKey: 'session-current',
24
+ contextWindow: {
25
+ usedContextTokens: 25,
26
+ totalContextTokens: 100,
27
+ availableContextTokens: 75,
28
+ prunedUsedContextTokens: 25,
29
+ droppedHistoryCount: 0,
30
+ truncatedToolResultCount: 0,
31
+ truncatedSystemPrompt: false,
32
+ truncatedUserMessage: false,
33
+ compacted: false,
34
+ compactedUsedContextTokens: 25,
35
+ compactedMessageCount: 0,
36
+ updatedAt: '2026-06-10T00:00:00.000Z',
37
+ },
38
+ });
39
+
40
+ const { result } = renderHook(() =>
41
+ useSelectedSessionContextWindowIndicator(),
42
+ );
43
+
44
+ expect(result.current).toMatchObject({
45
+ percentLabel: '25%',
46
+ ratio: 0.25,
47
+ tone: 'neutral',
48
+ });
49
+ });
50
+
51
+ it('hides the indicator when the current thread snapshot has no context window', () => {
52
+ useChatThreadStore.getState().setSnapshot({
53
+ sessionKey: 'session-current',
54
+ contextWindow: null,
55
+ });
56
+
57
+ const { result } = renderHook(() =>
58
+ useSelectedSessionContextWindowIndicator(),
59
+ );
60
+
61
+ expect(result.current).toBeNull();
62
+ });
63
+ });
@@ -1,18 +1,15 @@
1
1
  import { useMemo } from 'react';
2
2
  import type { ChatContextWindowIndicator } from '@nextclaw/agent-chat-ui';
3
- import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
4
3
  import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
5
4
  import { buildChatContextWindowIndicator } from '@/features/chat/features/session/utils/chat-context-window-indicator.utils';
6
5
 
7
6
  export function useSelectedSessionContextWindowIndicator(): ChatContextWindowIndicator | null {
8
- const selectedSessionKey = useChatSessionListStore((state) => state.snapshot.selectedSessionKey);
9
- const liveSessionKey = useChatThreadStore((state) => state.snapshot.sessionKey);
10
- const liveContextWindow = useChatThreadStore((state) => state.snapshot.contextWindow);
7
+ const liveContextWindow = useChatThreadStore(
8
+ (state) => state.snapshot.contextWindow,
9
+ );
11
10
 
12
- return useMemo(() => {
13
- if (selectedSessionKey && liveSessionKey === selectedSessionKey && liveContextWindow) {
14
- return buildChatContextWindowIndicator(liveContextWindow);
15
- }
16
- return null;
17
- }, [liveContextWindow, liveSessionKey, selectedSessionKey]);
11
+ return useMemo(
12
+ () => buildChatContextWindowIndicator(liveContextWindow),
13
+ [liveContextWindow],
14
+ );
18
15
  }
@@ -43,6 +43,14 @@ describe('adaptNcpSessionSummary', () => {
43
43
  projectRoot: '/Users/demo/workspace/project-alpha',
44
44
  workingDir: '/Users/demo/workspace/project-alpha',
45
45
  projectName: 'project-alpha',
46
+ metadata: {
47
+ label: 'NCP Planning Thread',
48
+ model: 'openai/gpt-5',
49
+ preferred_thinking: 'medium',
50
+ project_root: '/Users/demo/workspace/project-alpha',
51
+ session_type: 'native',
52
+ ui_last_read_at: '2026-03-17T23:59:00.000Z'
53
+ },
46
54
  lastMessageAt: '2026-03-18T00:00:00.000Z',
47
55
  readAt: '2026-03-17T23:59:00.000Z',
48
56
  sessionType: 'native',
@@ -308,6 +308,7 @@ export function adaptNcpMessagesToUiMessages(messages: readonly NcpMessageView[]
308
308
  }
309
309
 
310
310
  export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionEntryView {
311
+ const metadata = readMetadata(summary);
311
312
  const label = readNcpSessionLabel(summary);
312
313
  const preferredModel = readNcpSessionPreferredModel(summary);
313
314
  const preferredThinking = readNcpSessionPreferredThinking(summary);
@@ -335,6 +336,7 @@ export function adaptNcpSessionSummary(summary: NcpSessionSummaryView): SessionE
335
336
  ...(projectRoot ? { projectRoot } : {}),
336
337
  ...(workingDir ? { workingDir } : {}),
337
338
  ...(projectName ? { projectName } : {}),
339
+ ...(metadata ? { metadata } : {}),
338
340
  sessionType: readNcpSessionType(summary),
339
341
  sessionTypeMutable: false,
340
342
  status: summary.status ?? 'idle',
@@ -55,6 +55,7 @@ export function useChatSessionTypeState(params: UseChatSessionTypeStateParams):
55
55
  supportedModels: undefined,
56
56
  recommendedModel: null,
57
57
  modelSelectionMode: 'nextclaw',
58
+ runtimeDefaultThinking: null,
58
59
  cta: null
59
60
  });
60
61
  }
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  AgentProfileView,
3
3
  ChatSessionTypeOptionView,
4
+ RuntimeDefaultThinkingView,
4
5
  SessionTypeIconView,
5
6
  } from "@/shared/lib/api";
6
7
  import type { RuntimeModelSelectionMode } from "@nextclaw/shared";
@@ -18,6 +19,7 @@ export type ChatSessionTypeOption = {
18
19
  supportedModels?: string[];
19
20
  recommendedModel?: string | null;
20
21
  modelSelectionMode?: RuntimeModelSelectionMode;
22
+ runtimeDefaultThinking?: RuntimeDefaultThinkingView | null;
21
23
  cta?: {
22
24
  kind: string;
23
25
  label?: string;
@@ -76,6 +78,7 @@ export function buildSessionTypeOptions(
76
78
  supportedModels: option.supportedModels,
77
79
  recommendedModel: option.recommendedModel ?? null,
78
80
  modelSelectionMode: option.modelSelectionMode ?? "nextclaw",
81
+ runtimeDefaultThinking: option.runtimeDefaultThinking ?? null,
79
82
  cta: option.cta ?? null,
80
83
  });
81
84
  }
@@ -90,6 +93,7 @@ export function buildSessionTypeOptions(
90
93
  supportedModels: undefined,
91
94
  recommendedModel: null,
92
95
  modelSelectionMode: "nextclaw",
96
+ runtimeDefaultThinking: null,
93
97
  cta: null,
94
98
  });
95
99
  }
@@ -3,6 +3,7 @@ export { ChatSidebar } from "./components/layout/chat-sidebar";
3
3
  export { ChatPresenterProvider } from "./components/providers/chat-presenter.provider";
4
4
  export { usePresenter } from "./components/providers/chat-presenter.provider";
5
5
  export { ChatPresenter } from "./presenters/chat.presenter";
6
+ export { ChatDraftIntentManager } from "./managers/chat-draft-intent.manager";
6
7
  export { useChatInputStore } from "./stores/chat-input.store";
7
8
  export { useChatSessionListStore } from "./stores/chat-session-list.store";
8
9
  export { useChatThreadStore } from "./stores/chat-thread.store";
@@ -1,4 +1,5 @@
1
1
  import { createChatComposerTextNode } from '@nextclaw/agent-chat-ui';
2
+ import { RUNTIME_DEFAULT_MODEL_VALUE } from '@nextclaw/shared';
2
3
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
4
  import { ChatInputManager } from '@/features/chat/managers/chat-input.manager';
4
5
  import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
@@ -322,6 +323,33 @@ describe('ChatInputManager configuration sync', () => {
322
323
  });
323
324
  });
324
325
 
326
+ it('sends the runtime-default sentinel so the backend can ignore stale session model metadata', async () => {
327
+ useChatInputStore.setState({
328
+ snapshot: {
329
+ ...useChatInputStore.getState().snapshot,
330
+ selectedSessionType: 'codex',
331
+ selectedModel: RUNTIME_DEFAULT_MODEL_VALUE,
332
+ },
333
+ });
334
+ const chatRunManager = {
335
+ sendMessage: vi.fn().mockResolvedValue(undefined),
336
+ stopCurrentRun: vi.fn().mockResolvedValue(undefined),
337
+ } as unknown as ConstructorParameters<typeof ChatInputManager>[0];
338
+ const sessionListManager = {
339
+ ensureDraftSession: vi.fn(() => 'draft-session'),
340
+ } as unknown as ConstructorParameters<typeof ChatInputManager>[1];
341
+ const manager = new ChatInputManager(chatRunManager, sessionListManager);
342
+
343
+ await manager.send();
344
+
345
+ expect(chatRunManager.sendMessage).toHaveBeenCalledWith(
346
+ expect.objectContaining({
347
+ sessionType: 'codex',
348
+ model: RUNTIME_DEFAULT_MODEL_VALUE,
349
+ }),
350
+ );
351
+ });
352
+
325
353
  it('resolves and clears pending project root overrides inside the input manager', () => {
326
354
  useChatInputStore.setState({
327
355
  snapshot: {
@@ -0,0 +1,129 @@
1
+ import type { NcpMessage } from '@nextclaw/ncp';
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
4
+ import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
5
+ import { useChatThreadStore } from '@/features/chat/stores/chat-thread.store';
6
+
7
+ function createChatRunManager() {
8
+ const uiManager = {
9
+ isAtChatRoot: vi.fn(() => true),
10
+ goToSession: vi.fn(),
11
+ };
12
+ return new ChatRunManager(
13
+ uiManager as unknown as ConstructorParameters<typeof ChatRunManager>[0],
14
+ );
15
+ }
16
+
17
+ describe('ChatRunManager run snapshots', () => {
18
+ beforeEach(() => {
19
+ useChatInputStore.setState({
20
+ snapshot: {
21
+ ...useChatInputStore.getState().snapshot,
22
+ canStopGeneration: false,
23
+ sendError: null,
24
+ isSending: false,
25
+ },
26
+ });
27
+ useChatThreadStore.setState({
28
+ snapshot: {
29
+ ...useChatThreadStore.getState().snapshot,
30
+ sessionKey: null,
31
+ isHistoryLoading: false,
32
+ messages: [],
33
+ isSending: false,
34
+ isAwaitingAssistantOutput: false,
35
+ contextWindow: null,
36
+ },
37
+ });
38
+ });
39
+
40
+ it('syncs the run snapshot into chat input and thread state', () => {
41
+ const manager = createChatRunManager();
42
+ const message: NcpMessage = {
43
+ id: 'message-1',
44
+ sessionId: 'session-1',
45
+ role: 'assistant',
46
+ status: 'final',
47
+ parts: [{ type: 'text', text: 'done' }],
48
+ timestamp: '2026-06-10T00:00:00.000Z',
49
+ };
50
+
51
+ manager.applyRunSnapshot({
52
+ routeSessionKey: 'session-1',
53
+ isHydrating: true,
54
+ isSending: false,
55
+ isRunning: true,
56
+ visibleMessages: [message],
57
+ contextWindow: {
58
+ usedContextTokens: 10,
59
+ totalContextTokens: 100,
60
+ availableContextTokens: 90,
61
+ prunedUsedContextTokens: 10,
62
+ droppedHistoryCount: 0,
63
+ truncatedToolResultCount: 0,
64
+ truncatedSystemPrompt: false,
65
+ truncatedUserMessage: false,
66
+ compacted: false,
67
+ compactedUsedContextTokens: 10,
68
+ compactedMessageCount: 0,
69
+ updatedAt: '2026-06-10T00:00:00.000Z',
70
+ },
71
+ sendErrorMessage: 'failed',
72
+ materializedSessionKey: null,
73
+ });
74
+
75
+ expect(useChatInputStore.getState().snapshot).toMatchObject({
76
+ canStopGeneration: true,
77
+ sendError: 'failed',
78
+ isSending: true,
79
+ });
80
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
81
+ sessionKey: 'session-1',
82
+ isHistoryLoading: true,
83
+ messages: [message],
84
+ isSending: true,
85
+ isAwaitingAssistantOutput: true,
86
+ contextWindow: {
87
+ usedContextTokens: 10,
88
+ totalContextTokens: 100,
89
+ },
90
+ });
91
+ });
92
+
93
+ it('resets stale context window state when switching sessions', () => {
94
+ const manager = createChatRunManager();
95
+ useChatThreadStore.getState().setSnapshot({
96
+ sessionKey: 'session-previous',
97
+ contextWindow: {
98
+ usedContextTokens: 80,
99
+ totalContextTokens: 100,
100
+ availableContextTokens: 20,
101
+ prunedUsedContextTokens: 80,
102
+ droppedHistoryCount: 0,
103
+ truncatedToolResultCount: 0,
104
+ truncatedSystemPrompt: false,
105
+ truncatedUserMessage: false,
106
+ compacted: false,
107
+ compactedUsedContextTokens: 80,
108
+ compactedMessageCount: 0,
109
+ updatedAt: '2026-06-10T00:00:00.000Z',
110
+ },
111
+ });
112
+
113
+ manager.applyRunSnapshot({
114
+ routeSessionKey: 'session-next',
115
+ isHydrating: true,
116
+ isSending: false,
117
+ isRunning: false,
118
+ visibleMessages: [],
119
+ contextWindow: null,
120
+ sendErrorMessage: null,
121
+ materializedSessionKey: null,
122
+ });
123
+
124
+ expect(useChatThreadStore.getState().snapshot).toMatchObject({
125
+ sessionKey: 'session-next',
126
+ contextWindow: null,
127
+ });
128
+ });
129
+ });