@vibe-forge/client 0.3.0 → 0.5.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 (163) hide show
  1. package/cli.cjs +2 -1
  2. package/dist/assets/{arc-CwMXUVsq.js → arc-C4ymrcSQ.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CeB7-kgP.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-C935Im8S.js} +1 -1
  5. package/dist/assets/channel-84s1ACzD.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-B9IV13KI.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CXF_K4fE.js} +1 -1
  8. package/dist/assets/clone-B2E8tddE.js +1 -0
  9. package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-DwX8iC5F.js} +1 -1
  10. package/dist/assets/devicon-BWlTeAUU.woff +0 -0
  11. package/dist/assets/devicon-CirD-cQx.ttf +0 -0
  12. package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
  13. package/dist/assets/devicon-TqfHp33-.eot +0 -0
  14. package/dist/assets/{edges-96097737-B7e32Jeg.js → edges-96097737-9P1uH1RE.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-ixeGTFvg.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-G1gSTTBI.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CzrG99nD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-sFCoysWa.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-Ccsk_Lru.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-CwathJ6H.js} +1 -1
  22. package/dist/assets/{graph-BKbBNGPf.js → graph-DRCU-8Rz.js} +1 -1
  23. package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-Bq-fg2i_.js} +1 -1
  24. package/dist/assets/index-CHMuZ5-1.css +1 -0
  25. package/dist/assets/index-cGZvDhhU.js +542 -0
  26. package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-JBcUkJ6T.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DsdQU-R8.js} +1 -1
  28. package/dist/assets/{layout-By3JZZGt.js → layout-s0slG1OL.js} +1 -1
  29. package/dist/assets/{line-9GUsXbwv.js → line-CymFqgW6.js} +1 -1
  30. package/dist/assets/{linear-DzGV4E9N.js → linear-lDQVZ6aQ.js} +1 -1
  31. package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-Cmlqga_E.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-CqqTDJn_.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-BL2Ajx7Z.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-ClL_3ASt.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-CB1RgE3K.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-tgleEYiD.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-DlatQT5R.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-B--MLqRs.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-CRMZ6Dpx.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-CPiYHfUz.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-B9UKPAzX.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-BC1Ak1So.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-DV8R0g-n.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-CiqGS5DC.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-h6VSD3GE.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +12 -6
  49. package/src/App.tsx +2 -0
  50. package/src/api/README.md +26 -0
  51. package/src/api/automation.ts +88 -0
  52. package/src/api/base.ts +54 -0
  53. package/src/api/benchmark.ts +45 -0
  54. package/src/api/config.ts +24 -0
  55. package/src/api/knowledge.ts +72 -0
  56. package/src/api/projects.ts +15 -0
  57. package/src/api/sessions.ts +84 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -269
  60. package/src/components/AutomationView/AutomationView.scss +5 -1
  61. package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
  62. package/src/components/AutomationView/TaskList.scss +4 -6
  63. package/src/components/AutomationView/TaskList.tsx +2 -1
  64. package/src/components/AutomationView/TriggerList.scss +4 -1
  65. package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
  66. package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
  67. package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
  68. package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
  69. package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
  70. package/src/components/BenchmarkView/index.tsx +197 -0
  71. package/src/components/BenchmarkView/types.ts +10 -0
  72. package/src/components/BenchmarkView/utils.ts +21 -0
  73. package/src/components/Chat.tsx +43 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +32 -25
  76. package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
  77. package/src/components/NavRail.tsx +7 -0
  78. package/src/components/chat/ChatHeader.scss +37 -19
  79. package/src/components/chat/ChatHeader.tsx +6 -9
  80. package/src/components/chat/ChatHistoryView.tsx +99 -45
  81. package/src/components/chat/CurrentTodoList.tsx +10 -9
  82. package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
  83. package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
  84. package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
  85. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +146 -3
  86. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +183 -5
  87. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  88. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  89. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  90. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  91. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  92. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  93. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  94. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  95. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  96. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  97. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  98. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  99. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  100. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  101. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  102. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  103. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  104. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  105. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  106. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  107. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  108. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  109. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  110. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  111. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  112. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  113. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  114. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  116. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  117. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  118. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  119. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  120. package/src/components/config/ConfigSectionForm.tsx +12 -1
  121. package/src/components/config/ConfigSourceSwitch.tsx +12 -34
  122. package/src/components/config/channelDefinitions.ts +6 -0
  123. package/src/components/config/configSchema.ts +10 -1
  124. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  125. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  126. package/src/components/config/recordEditors/index.tsx +1 -0
  127. package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
  128. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  129. package/src/components/sidebar/SessionItem.scss +17 -0
  130. package/src/components/sidebar/SessionItem.tsx +21 -13
  131. package/src/hooks/chat/use-chat-adapter.ts +81 -0
  132. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  133. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +117 -22
  134. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  135. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  136. package/src/hooks/chat/use-chat-session-actions.ts +153 -0
  137. package/src/hooks/chat/use-chat-session-messages.ts +262 -0
  138. package/src/hooks/chat/use-chat-session.ts +63 -0
  139. package/src/hooks/chat/use-chat-view.ts +39 -0
  140. package/src/main.tsx +10 -13
  141. package/src/resources/adapters.ts +20 -0
  142. package/src/resources/locales/en.json +66 -0
  143. package/src/resources/locales/zh.json +66 -0
  144. package/src/runtime-config.ts +52 -0
  145. package/src/vite-env.d.ts +11 -0
  146. package/src/ws.ts +5 -3
  147. package/vite.config.ts +12 -4
  148. package/dist/assets/channel-jbCEHqbG.js +0 -1
  149. package/dist/assets/clone-CCRKqS4L.js +0 -1
  150. package/dist/assets/flowDiagram-v2-4f6560a1-Baslbgn4.js +0 -1
  151. package/dist/assets/index-B0qfCb1G.css +0 -1
  152. package/dist/assets/index-CNo75dYr.js +0 -497
  153. package/src/components/chat/ToolCallBox.scss +0 -137
  154. package/src/components/chat/useChatSession.ts +0 -370
  155. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  156. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  157. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  158. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  159. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  160. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  161. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  162. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  163. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -6,7 +6,7 @@ import { useNavigate } from 'react-router-dom'
6
6
  import type { ChatMessage, WSEvent } from '@vibe-forge/core'
7
7
 
8
8
  import { connectionManager } from '#~/connectionManager.js'
9
- import { CodeBlock } from '../../../CodeBlock'
9
+ import { CodeBlock } from '#~/components/CodeBlock'
10
10
 
11
11
  export interface TaskToolCardProps {
12
12
  description?: string
@@ -154,11 +154,13 @@ export function TaskToolCard({
154
154
  ))}
155
155
  </div>
156
156
  )}
157
- <CodeBlock
158
- hideHeader
159
- code={logText}
160
- lang='md'
161
- />
157
+ {logText !== '' && (
158
+ <CodeBlock
159
+ hideHeader
160
+ code={logText}
161
+ lang='md'
162
+ />
163
+ )}
162
164
  </div>
163
165
  </div>
164
166
  )
@@ -20,6 +20,7 @@ import {
20
20
  import type { TranslationFn } from './configUtils'
21
21
  import {
22
22
  BooleanRecordEditor,
23
+ ChannelRecordEditor,
23
24
  KeyValueEditor,
24
25
  McpServersRecordEditor,
25
26
  ModelServicesRecordEditor,
@@ -49,7 +50,7 @@ export const SectionForm = ({
49
50
  if (fields.length === 0) {
50
51
  return <Empty description={t('common.noData')} image={null} />
51
52
  }
52
- const directRecordSections = new Set(['modelServices', 'adapters', 'plugins', 'mcp'])
53
+ const directRecordSections = new Set(['modelServices', 'channels', 'adapters', 'plugins', 'mcp'])
53
54
 
54
55
  const modelServiceEntries = Object.entries(mergedModelServices)
55
56
  const modelServiceOptions: Array<{ value: string; label: ReactNode }> = modelServiceEntries.map(([key, entry]) => {
@@ -97,6 +98,7 @@ export const SectionForm = ({
97
98
 
98
99
  const getRecordKeyPlaceholder = (field: FieldSpec) => {
99
100
  if (sectionKey === 'modelServices') return t('config.editor.newModelServiceName')
101
+ if (sectionKey === 'channels') return t('config.editor.newChannelName')
100
102
  if (sectionKey === 'adapters') return t('config.editor.newAdapterName')
101
103
  if (sectionKey === 'plugins') {
102
104
  if (field.path.join('.') === 'extraKnownMarketplaces') return t('config.editor.newMarketplaceName')
@@ -247,6 +249,15 @@ export const SectionForm = ({
247
249
  keyPlaceholder={getRecordKeyPlaceholder(field)}
248
250
  />
249
251
  )
252
+ } else if (field.recordKind === 'channels') {
253
+ control = (
254
+ <ChannelRecordEditor
255
+ value={recordValue}
256
+ onChange={handleValueChange}
257
+ t={t}
258
+ keyPlaceholder={getRecordKeyPlaceholder(field)}
259
+ />
260
+ )
250
261
  } else if (field.recordKind === 'mcpServers') {
251
262
  control = (
252
263
  <McpServersRecordEditor
@@ -1,19 +1,16 @@
1
1
  import { Radio } from 'antd'
2
+ import type { ReactNode } from 'react'
2
3
 
3
4
  import type { ConfigSource } from '@vibe-forge/core'
4
5
 
5
- import type { TranslationFn } from './configUtils'
6
-
7
6
  export function ConfigSourceSwitch({
8
7
  value,
9
8
  onChange,
10
- configPresent,
11
- t
9
+ options,
12
10
  }: {
13
11
  value: ConfigSource
14
12
  onChange: (value: ConfigSource) => void
15
- configPresent?: { project?: boolean; user?: boolean }
16
- t: TranslationFn
13
+ options: Array<{ value: ConfigSource; icon: string; label: ReactNode }>
17
14
  }) {
18
15
  return (
19
16
  <Radio.Group
@@ -24,34 +21,15 @@ export function ConfigSourceSwitch({
24
21
  onChange={(event) => {
25
22
  onChange(event.target.value as ConfigSource)
26
23
  }}
27
- options={[
28
- {
29
- label: (
30
- <span className='config-view__source-option'>
31
- <span className='material-symbols-rounded'>folder</span>
32
- <span>
33
- {configPresent?.project === true
34
- ? t('config.sources.project')
35
- : t('config.sources.projectMissing')}
36
- </span>
37
- </span>
38
- ),
39
- value: 'project'
40
- },
41
- {
42
- label: (
43
- <span className='config-view__source-option'>
44
- <span className='material-symbols-rounded'>person</span>
45
- <span>
46
- {configPresent?.user === true
47
- ? t('config.sources.user')
48
- : t('config.sources.userMissing')}
49
- </span>
50
- </span>
51
- ),
52
- value: 'user'
53
- }
54
- ]}
24
+ options={options.map(opt => ({
25
+ value: opt.value,
26
+ label: (
27
+ <span className='config-view__source-option'>
28
+ <span className='material-symbols-rounded'>{opt.icon}</span>
29
+ <span>{opt.label}</span>
30
+ </span>
31
+ )
32
+ }))}
55
33
  />
56
34
  )
57
35
  }
@@ -0,0 +1,6 @@
1
+ import { channelDefinition } from '@vibe-forge/channel-lark'
2
+ import type { ChannelDescriptor } from '@vibe-forge/core/channel'
3
+
4
+ export const channelDefinitions: ChannelDescriptor[] = [
5
+ channelDefinition
6
+ ]
@@ -9,7 +9,7 @@ export type FieldValueType =
9
9
  | 'record'
10
10
  | 'shortcut'
11
11
 
12
- export type RecordKind = 'json' | 'modelServices' | 'mcpServers' | 'boolean' | 'keyValue'
12
+ export type RecordKind = 'json' | 'modelServices' | 'mcpServers' | 'boolean' | 'keyValue' | 'channels'
13
13
 
14
14
  export interface FieldOption {
15
15
  value: string
@@ -260,6 +260,15 @@ export const configSchema: Record<string, FieldSpec[]> = {
260
260
  icon: 'hub'
261
261
  }
262
262
  ],
263
+ channels: [
264
+ {
265
+ path: [],
266
+ type: 'record',
267
+ recordKind: 'channels',
268
+ defaultValue: {},
269
+ icon: 'campaign'
270
+ }
271
+ ],
263
272
  adapters: [
264
273
  {
265
274
  path: [],
@@ -0,0 +1 @@
1
+ @use './RecordEditors.scss';
@@ -0,0 +1,397 @@
1
+ import './ChannelRecordEditor.scss'
2
+
3
+ import { Button, Input, InputNumber, Select, Switch, Tooltip } from 'antd'
4
+ import { useEffect, useMemo, useState } from 'react'
5
+ import type { ZodRawShape, ZodTypeAny } from 'zod'
6
+ import { z } from 'zod'
7
+
8
+ import type { ChannelDescriptor } from '@vibe-forge/core/channel'
9
+ import { channelBaseSchema } from '@vibe-forge/core/channel'
10
+
11
+ import { ComplexTextEditor, StringArrayEditor } from '../ConfigEditors'
12
+ import { FieldRow } from '../ConfigFieldRow'
13
+ import { channelDefinitions } from '../channelDefinitions'
14
+ import { getTypeIcon, isSensitiveKey } from '../configUtils'
15
+ import type { TranslationFn } from '../configUtils'
16
+
17
+ type ChannelRecordValue = Record<string, unknown>
18
+
19
+ const getObjectShape = (schema: ZodTypeAny): ZodRawShape => {
20
+ if (schema instanceof z.ZodObject) return schema.shape
21
+ if (schema instanceof z.ZodEffects) return getObjectShape(schema.innerType())
22
+ if (schema instanceof z.ZodOptional) return getObjectShape(schema.unwrap())
23
+ if (schema instanceof z.ZodDefault) return getObjectShape(schema.removeDefault())
24
+ if (schema instanceof z.ZodNullable) return getObjectShape(schema.unwrap())
25
+ return {}
26
+ }
27
+
28
+ const unwrapSchema = (schema: ZodTypeAny): ZodTypeAny => {
29
+ if (schema instanceof z.ZodEffects) return unwrapSchema(schema.innerType())
30
+ if (schema instanceof z.ZodOptional) return unwrapSchema(schema.unwrap())
31
+ if (schema instanceof z.ZodDefault) return unwrapSchema(schema.removeDefault())
32
+ if (schema instanceof z.ZodNullable) return unwrapSchema(schema.unwrap())
33
+ return schema
34
+ }
35
+
36
+ const getDefaultValue = (schema: ZodTypeAny): unknown => {
37
+ if (schema instanceof z.ZodDefault) return schema._def.defaultValue()
38
+ if (schema instanceof z.ZodOptional) return undefined
39
+ if (schema instanceof z.ZodNullable) return null
40
+ if (schema instanceof z.ZodLiteral) return schema.value
41
+ if (schema instanceof z.ZodEnum) return schema.options[0]
42
+ if (schema instanceof z.ZodNativeEnum) {
43
+ const values = Object.values(schema.enum)
44
+ return values.length > 0 ? values[0] : undefined
45
+ }
46
+ if (schema instanceof z.ZodString) return ''
47
+ if (schema instanceof z.ZodNumber) return 0
48
+ if (schema instanceof z.ZodBoolean) return false
49
+ if (schema instanceof z.ZodArray) return []
50
+ if (schema instanceof z.ZodObject) {
51
+ const shape = getObjectShape(schema)
52
+ return Object.fromEntries(Object.entries(shape).map(([key, value]) => [key, getDefaultValue(value)]))
53
+ }
54
+ if (schema instanceof z.ZodRecord) return {}
55
+ if (schema instanceof z.ZodEffects) return getDefaultValue(schema.innerType())
56
+ return undefined
57
+ }
58
+
59
+ const toLabel = (key: string) =>
60
+ key
61
+ .replace(/_/g, ' ')
62
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
63
+ .replace(/\b\w/g, char => char.toUpperCase())
64
+
65
+ const buildRecordDefaults = (definition: ChannelDescriptor) => {
66
+ const shape = getObjectShape(definition.configSchema)
67
+ const entries = Object.entries(shape)
68
+ return entries.reduce<Record<string, unknown>>((acc, [key, schema]) => {
69
+ acc[key] = getDefaultValue(schema)
70
+ return acc
71
+ }, {})
72
+ }
73
+
74
+ const getChannelDefinition = (type: string | undefined) =>
75
+ channelDefinitions.find(definition => definition.type === type)
76
+
77
+ export const ChannelRecordEditor = ({
78
+ value,
79
+ onChange,
80
+ t,
81
+ keyPlaceholder
82
+ }: {
83
+ value: Record<string, unknown>
84
+ onChange: (nextValue: Record<string, unknown>) => void
85
+ t: TranslationFn
86
+ keyPlaceholder: string
87
+ }) => {
88
+ const [newKey, setNewKey] = useState('')
89
+ const [newType, setNewType] = useState(channelDefinitions[0]?.type ?? '')
90
+ const entries = useMemo(() => Object.entries(value), [value])
91
+ const [collapsedKeys, setCollapsedKeys] = useState<Record<string, boolean>>(() => (
92
+ Object.fromEntries(entries.map(([key]) => [key, true]))
93
+ ))
94
+ const baseKeys = useMemo(() => new Set(Object.keys(getObjectShape(channelBaseSchema))), [])
95
+ const typeOptions = useMemo(() =>
96
+ channelDefinitions.map(definition => ({
97
+ value: definition.type,
98
+ label: definition.label
99
+ })), [])
100
+
101
+ useEffect(() => {
102
+ setCollapsedKeys(prev => {
103
+ const next: Record<string, boolean> = {}
104
+ for (const [key] of entries) {
105
+ next[key] = prev[key] ?? true
106
+ }
107
+ return next
108
+ })
109
+ }, [entries])
110
+
111
+ const updateChannel = (key: string, nextValue: ChannelRecordValue) => {
112
+ onChange({ ...value, [key]: nextValue })
113
+ }
114
+
115
+ const renderField = (
116
+ channelKey: string,
117
+ recordValue: ChannelRecordValue,
118
+ fieldKey: string,
119
+ fieldSchema: ZodTypeAny
120
+ ) => {
121
+ const rawSchema = unwrapSchema(fieldSchema)
122
+ const description = rawSchema.description ?? ''
123
+ const label = toLabel(fieldKey)
124
+ const currentValue = recordValue[fieldKey]
125
+ const valueToUse = currentValue !== undefined ? currentValue : getDefaultValue(fieldSchema)
126
+
127
+ if (rawSchema instanceof z.ZodString) {
128
+ const isSensitive = isSensitiveKey(fieldKey)
129
+ return (
130
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
131
+ {isSensitive
132
+ ? (
133
+ <Input.Password
134
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
135
+ onChange={(event) => {
136
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
137
+ }}
138
+ placeholder={t('config.editor.secretPlaceholder')}
139
+ />
140
+ )
141
+ : (
142
+ <Input
143
+ value={typeof valueToUse === 'string' ? valueToUse : ''}
144
+ onChange={(event) => {
145
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: event.target.value })
146
+ }}
147
+ />
148
+ )}
149
+ </FieldRow>
150
+ )
151
+ }
152
+
153
+ if (rawSchema instanceof z.ZodNumber) {
154
+ return (
155
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('number')}>
156
+ <InputNumber
157
+ value={typeof valueToUse === 'number' ? valueToUse : 0}
158
+ onChange={(nextValue) => {
159
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue ?? 0 })
160
+ }}
161
+ />
162
+ </FieldRow>
163
+ )
164
+ }
165
+
166
+ if (rawSchema instanceof z.ZodBoolean) {
167
+ return (
168
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('boolean')}>
169
+ <Switch
170
+ checked={Boolean(valueToUse)}
171
+ onChange={(checked) => {
172
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: checked })
173
+ }}
174
+ />
175
+ </FieldRow>
176
+ )
177
+ }
178
+
179
+ if (rawSchema instanceof z.ZodEnum || rawSchema instanceof z.ZodNativeEnum) {
180
+ const optionValues: string[] = (
181
+ rawSchema instanceof z.ZodEnum
182
+ ? rawSchema.options
183
+ : Object.values(rawSchema.enum)
184
+ ).map((option: string | number) => String(option))
185
+ return (
186
+ <FieldRow key={fieldKey} title={label} description={description} icon={getTypeIcon('string')}>
187
+ <Select
188
+ value={typeof valueToUse === 'string' ? valueToUse : undefined}
189
+ options={optionValues.map((option) => ({ value: option, label: option }))}
190
+ onChange={(nextValue) => {
191
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
192
+ }}
193
+ />
194
+ </FieldRow>
195
+ )
196
+ }
197
+
198
+ if (rawSchema instanceof z.ZodArray) {
199
+ const inner = unwrapSchema(rawSchema.element)
200
+ if (inner instanceof z.ZodString) {
201
+ const items = Array.isArray(valueToUse)
202
+ ? valueToUse.filter(item => typeof item === 'string')
203
+ : []
204
+ return (
205
+ <FieldRow
206
+ key={fieldKey}
207
+ title={label}
208
+ description={description}
209
+ icon={getTypeIcon('array')}
210
+ layout='stacked'
211
+ >
212
+ <StringArrayEditor
213
+ value={items}
214
+ onChange={(nextValue) => {
215
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
216
+ }}
217
+ t={t}
218
+ />
219
+ </FieldRow>
220
+ )
221
+ }
222
+ return (
223
+ <FieldRow
224
+ key={fieldKey}
225
+ title={label}
226
+ description={description}
227
+ icon={getTypeIcon('array')}
228
+ layout='stacked'
229
+ >
230
+ <ComplexTextEditor
231
+ value={Array.isArray(valueToUse) ? valueToUse : []}
232
+ onChange={(nextValue) => {
233
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
234
+ }}
235
+ />
236
+ </FieldRow>
237
+ )
238
+ }
239
+
240
+ return (
241
+ <FieldRow
242
+ key={fieldKey}
243
+ title={label}
244
+ description={description}
245
+ icon={getTypeIcon('object')}
246
+ layout='stacked'
247
+ >
248
+ <ComplexTextEditor
249
+ value={valueToUse ?? {}}
250
+ onChange={(nextValue) => {
251
+ updateChannel(channelKey, { ...recordValue, [fieldKey]: nextValue })
252
+ }}
253
+ />
254
+ </FieldRow>
255
+ )
256
+ }
257
+
258
+ return (
259
+ <div className='config-view__record-list'>
260
+ <div className='config-view__record-add'>
261
+ <div className='config-view__record-add-inputs'>
262
+ <Input
263
+ value={newKey}
264
+ placeholder={keyPlaceholder}
265
+ onChange={(event) => setNewKey(event.target.value)}
266
+ />
267
+ <Select
268
+ value={newType}
269
+ options={typeOptions}
270
+ onChange={(nextValue) => setNewType(nextValue)}
271
+ />
272
+ <Tooltip title={t('common.confirm')}>
273
+ <Button
274
+ size='small'
275
+ type='primary'
276
+ className='config-view__icon-button'
277
+ aria-label={t('common.confirm')}
278
+ icon={<span className='material-symbols-rounded'>check</span>}
279
+ disabled={newKey.trim() === '' || Object.hasOwn(value, newKey) || newType === ''}
280
+ onClick={() => {
281
+ const definition = getChannelDefinition(newType)
282
+ if (!definition) return
283
+ onChange({
284
+ ...value,
285
+ [newKey]: buildRecordDefaults(definition)
286
+ })
287
+ setNewKey('')
288
+ }}
289
+ />
290
+ </Tooltip>
291
+ </div>
292
+ </div>
293
+ {entries.map(([key, itemValue]) => {
294
+ const recordValue = (itemValue != null && typeof itemValue === 'object')
295
+ ? itemValue as ChannelRecordValue
296
+ : {}
297
+ const type = typeof recordValue.type === 'string' ? recordValue.type : undefined
298
+ const definition = getChannelDefinition(type)
299
+ const titleValue = typeof recordValue.title === 'string' ? recordValue.title : ''
300
+ const descriptionValue = typeof recordValue.description === 'string' ? recordValue.description : ''
301
+ const displayName = titleValue.trim() !== '' ? titleValue : key
302
+ const typeLabel = definition?.label ?? type ?? t('config.editor.unknownChannelType')
303
+ const isCollapsed = collapsedKeys[key] === true
304
+ const shape = definition ? getObjectShape(definition.configSchema) : {}
305
+ const fieldEntries = Object.entries(shape)
306
+ .filter(([fieldKey]) => fieldKey !== 'type')
307
+ .sort(([a], [b]) => {
308
+ const order = ['title', 'description', 'enabled', 'admins']
309
+ const aIndex = order.indexOf(a)
310
+ const bIndex = order.indexOf(b)
311
+ if (aIndex === -1 && bIndex === -1) return a.localeCompare(b)
312
+ if (aIndex === -1) return 1
313
+ if (bIndex === -1) return -1
314
+ return aIndex - bIndex
315
+ })
316
+
317
+ return (
318
+ <div
319
+ key={key}
320
+ className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
321
+ >
322
+ <div className='config-view__record-title'>
323
+ <div className='config-view__record-title-left'>
324
+ <Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
325
+ <Button
326
+ size='small'
327
+ type='text'
328
+ className='config-view__icon-button config-view__icon-button--compact'
329
+ aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
330
+ icon={
331
+ <span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>
332
+ }
333
+ onClick={() => {
334
+ setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
335
+ }}
336
+ />
337
+ </Tooltip>
338
+ <div className='config-view__record-heading'>
339
+ <div>{displayName}</div>
340
+ <div className='config-view__record-subtitle'>
341
+ {key} · {typeLabel}
342
+ </div>
343
+ {descriptionValue !== '' && (
344
+ <div className='config-view__record-desc'>{descriptionValue}</div>
345
+ )}
346
+ </div>
347
+ </div>
348
+ <div className='config-view__record-actions'>
349
+ <Tooltip title={t('config.editor.remove')}>
350
+ <Button
351
+ size='small'
352
+ type='text'
353
+ danger
354
+ className='config-view__icon-button config-view__icon-button--compact'
355
+ aria-label={t('config.editor.remove')}
356
+ icon={<span className='material-symbols-rounded'>delete</span>}
357
+ onClick={() => {
358
+ const updated = { ...value }
359
+ delete updated[key]
360
+ onChange(updated)
361
+ }}
362
+ />
363
+ </Tooltip>
364
+ </div>
365
+ </div>
366
+ <div className='config-view__record-body'>
367
+ <div className='config-view__record-fields'>
368
+ <FieldRow
369
+ title={t('config.editor.channelType')}
370
+ description={definition?.description ?? ''}
371
+ icon={getTypeIcon('string')}
372
+ >
373
+ <Select
374
+ value={type}
375
+ options={typeOptions}
376
+ onChange={(nextType) => {
377
+ const nextDefinition = getChannelDefinition(nextType)
378
+ if (!nextDefinition) return
379
+ const defaults = buildRecordDefaults(nextDefinition)
380
+ const preserved = Object.fromEntries(
381
+ Object.entries(recordValue).filter(([fieldKey]) => baseKeys.has(fieldKey))
382
+ )
383
+ updateChannel(key, { ...defaults, ...preserved, type: nextType })
384
+ }}
385
+ />
386
+ </FieldRow>
387
+ {fieldEntries.map(([fieldKey, fieldSchema]) => (
388
+ renderField(key, recordValue, fieldKey, fieldSchema)
389
+ ))}
390
+ </div>
391
+ </div>
392
+ </div>
393
+ )
394
+ })}
395
+ </div>
396
+ )
397
+ }
@@ -1,3 +1,4 @@
1
+ export { ChannelRecordEditor } from './ChannelRecordEditor'
1
2
  export { BooleanRecordEditor } from './BooleanRecordEditor'
2
3
  export { KeyValueEditor } from './KeyValueEditor'
3
4
  export { McpServersRecordEditor } from './McpServersRecordEditor'
@@ -5,7 +5,7 @@ import useSWR from 'swr'
5
5
 
6
6
  import type { RuleDetail, RuleSummary } from '#~/api.js'
7
7
  import { getRuleDetail } from '#~/api.js'
8
- import { MarkdownContent } from '#~/components/chat/MarkdownContent'
8
+ import { MarkdownContent } from '#~/components/MarkdownContent'
9
9
  import { LoadingState } from './LoadingState'
10
10
 
11
11
  type RuleItemProps = {
@@ -7,7 +7,7 @@ import useSWR from 'swr'
7
7
 
8
8
  import type { SpecDetail, SpecSummary } from '#~/api.js'
9
9
  import { getSpecDetail } from '#~/api.js'
10
- import { MarkdownContent } from '#~/components/chat/MarkdownContent'
10
+ import { MarkdownContent } from '#~/components/MarkdownContent'
11
11
  import { LoadingState } from './LoadingState'
12
12
  import { MetaList } from './MetaList'
13
13
 
@@ -163,11 +163,15 @@
163
163
  align-items: center;
164
164
 
165
165
  .session-tag {
166
+ max-width: 120px;
166
167
  font-size: 10px;
167
168
  margin: 0;
168
169
  padding: 0 4px;
169
170
  line-height: 14px;
170
171
  border-radius: 2px;
172
+ display: inline-flex;
173
+ align-items: center;
174
+ white-space: nowrap;
171
175
 
172
176
  &--automation {
173
177
  padding: 0 6px;
@@ -176,6 +180,19 @@
176
180
  &__link {
177
181
  color: var(--primary-color);
178
182
  text-decoration: none;
183
+ display: inline-block;
184
+ max-width: 100%;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ white-space: nowrap;
188
+ }
189
+
190
+ &__text {
191
+ display: inline-block;
192
+ max-width: 100%;
193
+ overflow: hidden;
194
+ text-overflow: ellipsis;
195
+ white-space: nowrap;
179
196
  }
180
197
  }
181
198
  }
@@ -226,28 +226,36 @@ export function SessionItem({
226
226
  if (automationTag) {
227
227
  const href = `/automation?rule=${encodeURIComponent(automationTag.ruleId)}`
228
228
  return (
229
- <Tag
229
+ <Tooltip
230
230
  key={tag}
231
- className='session-tag session-tag--automation'
232
- onClick={(event) => event.stopPropagation()}
231
+ title={automationTag.ruleTitle}
233
232
  >
234
- <a
235
- className='session-tag__link'
236
- href={href}
233
+ <Tag
234
+ className='session-tag session-tag--automation'
237
235
  onClick={(event) => event.stopPropagation()}
238
236
  >
239
- {automationTag.ruleTitle}
240
- </a>
241
- </Tag>
237
+ <a
238
+ className='session-tag__link'
239
+ href={href}
240
+ onClick={(event) => event.stopPropagation()}
241
+ >
242
+ {automationTag.ruleTitle}
243
+ </a>
244
+ </Tag>
245
+ </Tooltip>
242
246
  )
243
247
  }
244
248
  return (
245
- <Tag
249
+ <Tooltip
246
250
  key={tag}
247
- className='session-tag'
251
+ title={tag}
248
252
  >
249
- {tag}
250
- </Tag>
253
+ <Tag className='session-tag'>
254
+ <span className='session-tag__text'>
255
+ {tag}
256
+ </span>
257
+ </Tag>
258
+ </Tooltip>
251
259
  )
252
260
  })}
253
261
  </div>