@vibe-forge/client 0.2.0-alpha.9 → 0.4.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 (166) hide show
  1. package/cli.cjs +1 -1
  2. package/dist/assets/{arc-CybT1Fs2.js → arc-DgIxeTMg.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-BY5Aoa-D.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-F42hTbzS.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
  5. package/dist/assets/channel-DhtnrNJ6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-D-tIPp-3.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-J57aCe6u.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
  8. package/dist/assets/clone-7bHB6YkC.js +1 -0
  9. package/dist/assets/{createText-1719965b-ByfEqOF-.js → createText-1719965b-C1_HJcCc.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-CMEArkOa.js → edges-96097737-BU8qStzd.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-Cf8mX2aj.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-DG6WKyo7.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-CstUxz-w.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1--4CRoQ-H.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-DYgHcKd-.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DDSVpfsd.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
  22. package/dist/assets/{graph-CRWF39gX.js → graph-CIkpD4Kx.js} +1 -1
  23. package/dist/assets/{index-5325376f-W1hft795.js → index-5325376f-aVVRRTIu.js} +1 -1
  24. package/dist/assets/index-D1giUI7r.css +1 -0
  25. package/dist/assets/index-DRSI_ZIL.js +514 -0
  26. package/dist/assets/{infoDiagram-8eee0895-D4SHcix6.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-MWgCkVoE.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
  28. package/dist/assets/{layout-C88ObkCf.js → layout-Tnmha8Nh.js} +1 -1
  29. package/dist/assets/{line-C7WAYMt5.js → line-BQR2SOyl.js} +1 -1
  30. package/dist/assets/{linear-C4msxfcU.js → linear-DlG0eemV.js} +1 -1
  31. package/dist/assets/{mermaid.core-Cabag9SZ.js → mermaid.core-BnwYO0He.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-CeS8ETXx.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-BvjyKnq5.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-DzYvpbNM.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-DHIoDbyo.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-BFSGnQGs.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-_LM3BJ5-.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-DwORjOzl.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-B4cAWWz1.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-D_rmV3B_.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-BFx4VuFc.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-BE3106vL.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-DwDTO1op.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-C4b8qUQZ.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-BRJ9Z4u-.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +11 -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 +82 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -241
  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 +37 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +13 -1
  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 +89 -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/NewSessionGuide.scss +35 -13
  86. package/src/components/chat/NewSessionGuide.tsx +20 -10
  87. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
  88. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
  89. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  90. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  91. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  92. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  93. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  94. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  95. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  96. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  97. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  98. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  99. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  100. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  101. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  102. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  103. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  104. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  105. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  106. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  107. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  108. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  109. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  110. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  111. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  112. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  113. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  114. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  116. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  117. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  118. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  119. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  120. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  121. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  122. package/src/components/config/AppSettingsPanel.tsx +33 -0
  123. package/src/components/config/ConfigSectionForm.tsx +12 -1
  124. package/src/components/config/channelDefinitions.ts +6 -0
  125. package/src/components/config/configSchema.ts +10 -1
  126. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  127. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  128. package/src/components/config/recordEditors/index.tsx +1 -0
  129. package/src/components/knowledge-base/KnowledgeBaseView.tsx +51 -3
  130. package/src/components/knowledge-base/components/RuleItem.tsx +79 -0
  131. package/src/components/knowledge-base/components/RuleList.scss +5 -0
  132. package/src/components/knowledge-base/components/RuleList.tsx +70 -0
  133. package/src/components/knowledge-base/components/RulesTab.tsx +32 -7
  134. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  135. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  136. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +65 -16
  137. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  138. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  139. package/src/hooks/chat/use-chat-session-actions.ts +147 -0
  140. package/src/hooks/chat/use-chat-session-messages.ts +250 -0
  141. package/src/hooks/chat/use-chat-session.ts +57 -0
  142. package/src/hooks/chat/use-chat-view.ts +39 -0
  143. package/src/main.tsx +10 -13
  144. package/src/resources/locales/en.json +73 -0
  145. package/src/resources/locales/zh.json +73 -0
  146. package/src/runtime-config.ts +52 -0
  147. package/src/store/index.ts +2 -0
  148. package/src/vite-env.d.ts +11 -0
  149. package/src/ws.ts +5 -3
  150. package/vite.config.ts +12 -4
  151. package/dist/assets/channel-DrWdSpqV.js +0 -1
  152. package/dist/assets/clone-D0cC8LLB.js +0 -1
  153. package/dist/assets/flowDiagram-v2-4f6560a1-Bf_DH7dp.js +0 -1
  154. package/dist/assets/index-CNMzWvKV.js +0 -497
  155. package/dist/assets/index-PEmISxiy.css +0 -1
  156. package/src/components/chat/ToolCallBox.scss +0 -137
  157. package/src/components/chat/useChatSession.ts +0 -370
  158. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  159. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  160. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  161. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  162. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  163. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  164. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  165. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  166. /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
  )
@@ -0,0 +1,33 @@
1
+ import '../ConfigView.scss'
2
+
3
+ import { Switch } from 'antd'
4
+ import { useAtom } from 'jotai'
5
+
6
+ import { showAnnouncementsAtom } from '#~/store/index.js'
7
+
8
+ import { FieldRow } from './ConfigFieldRow'
9
+ import type { TranslationFn } from './configUtils'
10
+
11
+ export function AppSettingsPanel({ t }: { t: TranslationFn }) {
12
+ const [showAnnouncements, setShowAnnouncements] = useAtom(showAnnouncementsAtom)
13
+
14
+ return (
15
+ <div className='config-view__editor-wrap'>
16
+ <div className='config-view__section-header'>
17
+ <div className='config-view__section-title'>
18
+ <span className='material-symbols-rounded config-view__section-icon'>tune</span>
19
+ <span>{t('config.sections.appearance')}</span>
20
+ </div>
21
+ </div>
22
+ <div className='config-view__card'>
23
+ <FieldRow
24
+ title={t('config.appSettings.announcements.label')}
25
+ description={t('config.appSettings.announcements.desc')}
26
+ icon='campaign'
27
+ >
28
+ <Switch checked={showAnnouncements} onChange={setShowAnnouncements} />
29
+ </FieldRow>
30
+ </div>
31
+ </div>
32
+ )
33
+ }
@@ -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
@@ -0,0 +1,6 @@
1
+ import { larkChannelDefinition } from '@vibe-forge/channel-lark'
2
+ import type { ChannelDescriptor } from '@vibe-forge/core/channel'
3
+
4
+ export const channelDefinitions: ChannelDescriptor[] = [
5
+ larkChannelDefinition
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
+ {entries.map(([key, itemValue]) => {
261
+ const recordValue = (itemValue != null && typeof itemValue === 'object')
262
+ ? itemValue as ChannelRecordValue
263
+ : {}
264
+ const type = typeof recordValue.type === 'string' ? recordValue.type : undefined
265
+ const definition = getChannelDefinition(type)
266
+ const titleValue = typeof recordValue.title === 'string' ? recordValue.title : ''
267
+ const descriptionValue = typeof recordValue.description === 'string' ? recordValue.description : ''
268
+ const displayName = titleValue.trim() !== '' ? titleValue : key
269
+ const typeLabel = definition?.label ?? type ?? t('config.editor.unknownChannelType')
270
+ const isCollapsed = collapsedKeys[key] === true
271
+ const shape = definition ? getObjectShape(definition.configSchema) : {}
272
+ const fieldEntries = Object.entries(shape)
273
+ .filter(([fieldKey]) => fieldKey !== 'type')
274
+ .sort(([a], [b]) => {
275
+ const order = ['title', 'description', 'enabled', 'admins']
276
+ const aIndex = order.indexOf(a)
277
+ const bIndex = order.indexOf(b)
278
+ if (aIndex === -1 && bIndex === -1) return a.localeCompare(b)
279
+ if (aIndex === -1) return 1
280
+ if (bIndex === -1) return -1
281
+ return aIndex - bIndex
282
+ })
283
+
284
+ return (
285
+ <div
286
+ key={key}
287
+ className={`config-view__record-card${isCollapsed ? ' config-view__record-card--collapsed' : ''}`}
288
+ >
289
+ <div className='config-view__record-title'>
290
+ <div className='config-view__record-title-left'>
291
+ <Tooltip title={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}>
292
+ <Button
293
+ size='small'
294
+ type='text'
295
+ className='config-view__icon-button config-view__icon-button--compact'
296
+ aria-label={isCollapsed ? t('config.editor.expand') : t('config.editor.collapse')}
297
+ icon={
298
+ <span className='material-symbols-rounded'>{isCollapsed ? 'chevron_right' : 'expand_more'}</span>
299
+ }
300
+ onClick={() => {
301
+ setCollapsedKeys(prev => ({ ...prev, [key]: !isCollapsed }))
302
+ }}
303
+ />
304
+ </Tooltip>
305
+ <div className='config-view__record-heading'>
306
+ <div>{displayName}</div>
307
+ <div className='config-view__record-subtitle'>
308
+ {key} · {typeLabel}
309
+ </div>
310
+ {descriptionValue !== '' && (
311
+ <div className='config-view__record-desc'>{descriptionValue}</div>
312
+ )}
313
+ </div>
314
+ </div>
315
+ <div className='config-view__record-actions'>
316
+ <Tooltip title={t('config.editor.remove')}>
317
+ <Button
318
+ size='small'
319
+ type='text'
320
+ danger
321
+ className='config-view__icon-button config-view__icon-button--compact'
322
+ aria-label={t('config.editor.remove')}
323
+ icon={<span className='material-symbols-rounded'>delete</span>}
324
+ onClick={() => {
325
+ const updated = { ...value }
326
+ delete updated[key]
327
+ onChange(updated)
328
+ }}
329
+ />
330
+ </Tooltip>
331
+ </div>
332
+ </div>
333
+ <div className='config-view__record-body'>
334
+ <div className='config-view__record-fields'>
335
+ <FieldRow
336
+ title={t('config.editor.channelType')}
337
+ description={definition?.description ?? ''}
338
+ icon={getTypeIcon('string')}
339
+ >
340
+ <Select
341
+ value={type}
342
+ options={typeOptions}
343
+ onChange={(nextType) => {
344
+ const nextDefinition = getChannelDefinition(nextType)
345
+ if (!nextDefinition) return
346
+ const defaults = buildRecordDefaults(nextDefinition)
347
+ const preserved = Object.fromEntries(
348
+ Object.entries(recordValue).filter(([fieldKey]) => baseKeys.has(fieldKey))
349
+ )
350
+ updateChannel(key, { ...defaults, ...preserved, type: nextType })
351
+ }}
352
+ />
353
+ </FieldRow>
354
+ {fieldEntries.map(([fieldKey, fieldSchema]) => (
355
+ renderField(key, recordValue, fieldKey, fieldSchema)
356
+ ))}
357
+ </div>
358
+ </div>
359
+ </div>
360
+ )
361
+ })}
362
+ <div className='config-view__record-add'>
363
+ <div className='config-view__record-add-inputs'>
364
+ <Input
365
+ value={newKey}
366
+ placeholder={keyPlaceholder}
367
+ onChange={(event) => setNewKey(event.target.value)}
368
+ />
369
+ <Select
370
+ value={newType}
371
+ options={typeOptions}
372
+ onChange={(nextValue) => setNewType(nextValue)}
373
+ />
374
+ <Tooltip title={t('common.confirm')}>
375
+ <Button
376
+ size='small'
377
+ type='primary'
378
+ className='config-view__icon-button'
379
+ aria-label={t('common.confirm')}
380
+ icon={<span className='material-symbols-rounded'>check</span>}
381
+ disabled={newKey.trim() === '' || Object.hasOwn(value, newKey) || newType === ''}
382
+ onClick={() => {
383
+ const definition = getChannelDefinition(newType)
384
+ if (!definition) return
385
+ onChange({
386
+ ...value,
387
+ [newKey]: buildRecordDefaults(definition)
388
+ })
389
+ setNewKey('')
390
+ }}
391
+ />
392
+ </Tooltip>
393
+ </div>
394
+ </div>
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,8 @@ import React from 'react'
5
5
  import { useTranslation } from 'react-i18next'
6
6
  import useSWR from 'swr'
7
7
 
8
- import type { EntitySummary, SpecSummary } from '#~/api.js'
8
+ import type { EntitySummary, RuleSummary, SpecSummary } from '#~/api.js'
9
+ import { useQueryParams } from '#~/hooks/useQueryParams.js'
9
10
  import { EntitiesTab } from './components/EntitiesTab.js'
10
11
  import { FlowsTab } from './components/FlowsTab.js'
11
12
  import { KnowledgeBaseHeader } from './components/KnowledgeBaseHeader.js'
@@ -13,6 +14,10 @@ import { RulesTab } from './components/RulesTab.js'
13
14
  import { SkillsTab } from './components/SkillsTab.js'
14
15
  import { TabLabel } from './components/TabLabel.js'
15
16
 
17
+ interface KnowledgeQueryParams extends Record<string, string> {
18
+ kbTab: string
19
+ }
20
+
16
21
  export function KnowledgeBaseView() {
17
22
  const { t } = useTranslation()
18
23
  const { message } = App.useApp()
@@ -26,14 +31,28 @@ export function KnowledgeBaseView() {
26
31
  isLoading: isEntitiesLoading,
27
32
  mutate: mutateEntities
28
33
  } = useSWR<{ entities: EntitySummary[] }>('/api/ai/entities')
34
+ const {
35
+ data: rulesRes,
36
+ isLoading: isRulesLoading,
37
+ mutate: mutateRules
38
+ } = useSWR<{ rules: RuleSummary[] }>('/api/ai/rules')
29
39
 
30
40
  const specs = specsRes?.specs ?? []
31
41
  const entities = entitiesRes?.entities ?? []
42
+ const rules = rulesRes?.rules ?? []
32
43
 
33
44
  const [specQuery, setSpecQuery] = React.useState('')
34
45
  const [specTagFilter, setSpecTagFilter] = React.useState<string[]>([])
35
46
  const [entityQuery, setEntityQuery] = React.useState('')
36
47
  const [entityTagFilter, setEntityTagFilter] = React.useState<string[]>([])
48
+ const [ruleQuery, setRuleQuery] = React.useState('')
49
+
50
+ const { values, update } = useQueryParams<KnowledgeQueryParams>({
51
+ keys: ['kbTab'],
52
+ defaults: {
53
+ kbTab: 'skills'
54
+ }
55
+ })
37
56
 
38
57
  const specTagOptions = React.useMemo(() => {
39
58
  const tags = new Set<string>()
@@ -81,8 +100,18 @@ export function KnowledgeBaseView() {
81
100
  })
82
101
  }, [entityQuery, entityTagFilter, entities])
83
102
 
103
+ const filteredRules = React.useMemo(() => {
104
+ const query = ruleQuery.trim().toLowerCase()
105
+ return rules.filter(rule => {
106
+ if (query === '') return true
107
+ const globText = (rule.globs ?? []).join(' ')
108
+ const haystack = `${rule.name} ${rule.description} ${globText}`.toLowerCase()
109
+ return haystack.includes(query)
110
+ })
111
+ }, [ruleQuery, rules])
112
+
84
113
  const handleRefresh = async () => {
85
- await Promise.all([mutateSpecs(), mutateEntities()])
114
+ await Promise.all([mutateSpecs(), mutateEntities(), mutateRules()])
86
115
  void message.success(t('knowledge.actions.refreshed'))
87
116
  }
88
117
 
@@ -170,6 +199,11 @@ export function KnowledgeBaseView() {
170
199
  label: <TabLabel icon='gavel' label={t('knowledge.tabs.rules')} />,
171
200
  children: (
172
201
  <RulesTab
202
+ rules={rules}
203
+ filteredRules={filteredRules}
204
+ isLoading={isRulesLoading}
205
+ query={ruleQuery}
206
+ onQueryChange={setRuleQuery}
173
207
  onCreate={handleCreateRule}
174
208
  onImport={handleImportRule}
175
209
  />
@@ -177,10 +211,24 @@ export function KnowledgeBaseView() {
177
211
  }
178
212
  ]
179
213
 
214
+ const tabKeys = React.useMemo(() => tabs.map(tab => tab.key), [tabs])
215
+ const activeTab = tabKeys.includes(values.kbTab) ? values.kbTab : tabKeys[0]
216
+
217
+ React.useEffect(() => {
218
+ if (values.kbTab !== activeTab) {
219
+ update({ kbTab: activeTab })
220
+ }
221
+ }, [activeTab, update, values.kbTab])
222
+
180
223
  return (
181
224
  <div className='knowledge-base-view'>
182
225
  <KnowledgeBaseHeader onRefresh={handleRefresh} />
183
- <Tabs className='knowledge-base-view__tabs' items={tabs} />
226
+ <Tabs
227
+ className='knowledge-base-view__tabs'
228
+ items={tabs}
229
+ activeKey={activeTab}
230
+ onChange={(key) => update({ kbTab: key })}
231
+ />
184
232
  </div>
185
233
  )
186
234
  }