cortico 0.1.0 → 0.1.2

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 (112) hide show
  1. package/README.md +7 -14
  2. package/package.json +1 -3
  3. package/src/boot.ts +18 -0
  4. package/src/bot.ts +41 -6
  5. package/src/core/README.md +4 -6
  6. package/src/core/blobs.ts +23 -2
  7. package/src/core/config-schema.ts +1 -1
  8. package/src/core/config.ts +2 -16
  9. package/src/core/core.ts +15 -5
  10. package/src/core/loop.ts +55 -27
  11. package/src/core/session.ts +3 -2
  12. package/src/core/timers.ts +8 -6
  13. package/src/core/types.ts +19 -3
  14. package/src/core/util.ts +17 -1
  15. package/src/deploy.ts +4 -1
  16. package/src/extensions/README.md +6 -4
  17. package/src/extensions/dry-mount.ts +23 -2
  18. package/src/extensions/manifest.ts +21 -11
  19. package/src/extensions.ts +147 -23
  20. package/src/launcher.ts +23 -9
  21. package/src/protocol/open-responses/context-log.ts +37 -9
  22. package/src/providers/README.md +32 -8
  23. package/src/providers/base.ts +3 -1
  24. package/src/providers/configuration.ts +2 -1
  25. package/src/providers/console/hub.ts +270 -0
  26. package/src/providers/console/settings.ts +8 -18
  27. package/src/providers/console/types.ts +5 -0
  28. package/src/providers/hub-api.ts +3 -0
  29. package/src/providers/llamacpp/config.ts +8 -6
  30. package/src/providers/llamacpp/console/runtime-panel.ts +5 -1
  31. package/src/providers/llamacpp/console/server.ts +3 -1
  32. package/src/providers/llamacpp/index.ts +3 -1
  33. package/src/providers/llamacpp/native.ts +10 -5
  34. package/src/providers/llamacpp/strings.ts +6 -6
  35. package/src/providers/name.ts +8 -0
  36. package/src/providers/openai-responses-compat/config.ts +13 -0
  37. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  38. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  39. package/src/providers/openai-responses-compat/console/server.ts +127 -0
  40. package/src/providers/openai-responses-compat/index.ts +38 -13
  41. package/src/providers/openai-responses-compat/native.ts +9 -4
  42. package/src/providers/openai-responses-compat/strings.ts +62 -1
  43. package/src/providers/registry.ts +5 -0
  44. package/src/providers/transport/responses-input.ts +59 -10
  45. package/src/web/README.md +11 -6
  46. package/src/web/auth.ts +80 -0
  47. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +5 -4
  48. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  49. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +6 -8
  50. package/src/web/client/console-pages/host.ts +24 -7
  51. package/src/web/client/core/api.ts +5 -1
  52. package/src/web/client/core/router.ts +15 -0
  53. package/src/web/client/features/core/strings.ts +2 -2
  54. package/src/web/client/features/extensions/index.ts +276 -41
  55. package/src/web/client/features/extensions/strings.ts +84 -10
  56. package/src/web/client/features/feature.ts +2 -2
  57. package/src/web/client/features/live/diagnostics.ts +32 -0
  58. package/src/web/client/features/live/index.ts +34 -11
  59. package/src/web/client/features/live/onboarding.ts +4 -1
  60. package/src/web/client/features/live/protocol.ts +1 -0
  61. package/src/web/client/features/live/strings.ts +20 -14
  62. package/src/web/client/features/live/timeline.ts +2 -1
  63. package/src/web/client/features/providers/detail.ts +262 -0
  64. package/src/web/client/features/providers/drafts.ts +23 -0
  65. package/src/web/client/features/providers/index.ts +173 -87
  66. package/src/web/client/features/providers/strings.ts +44 -17
  67. package/src/web/client/features/providers/types.ts +15 -0
  68. package/src/web/client/features/settings/general.ts +13 -0
  69. package/src/web/client/features/settings/index.ts +1 -0
  70. package/src/web/client/features/settings/strings.ts +6 -0
  71. package/src/web/client/features/worlds/index.ts +1 -0
  72. package/src/web/client/main.ts +4 -0
  73. package/src/web/client/shell/index.ts +28 -48
  74. package/src/web/client/shell/strings.ts +0 -22
  75. package/src/web/client/ui/icons.ts +14 -1
  76. package/src/web/client/ui/prompt-input.tsx +17 -5
  77. package/src/web/client/ui/strings.ts +0 -2
  78. package/src/web/console-pages.ts +1 -1
  79. package/src/web/diagnostics.ts +133 -0
  80. package/src/web/public/login.html +67 -0
  81. package/src/web/public/styles.css +143 -26
  82. package/src/web/server.ts +233 -19
  83. package/src/web/shared/client-panel.ts +4 -1
  84. package/src/web/shared/console-protocol.ts +4 -1
  85. package/src/worlds/bilibili/README.md +1 -1
  86. package/src/worlds/bilibili/overlay/server.ts +4 -2
  87. package/src/worlds/minecraft/ADAPT.md +66 -0
  88. package/src/worlds/minecraft/README.md +69 -11
  89. package/src/worlds/minecraft/cell-facts.ts +226 -0
  90. package/src/worlds/minecraft/chests.ts +5 -0
  91. package/src/worlds/minecraft/containers.ts +325 -0
  92. package/src/worlds/minecraft/entity-facts.ts +31 -5
  93. package/src/worlds/minecraft/executor.ts +283 -10348
  94. package/src/worlds/minecraft/inventory.ts +268 -0
  95. package/src/worlds/minecraft/melee.ts +419 -0
  96. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  97. package/src/worlds/minecraft/placed-ledger.ts +152 -0
  98. package/src/worlds/minecraft/placement.ts +1038 -0
  99. package/src/worlds/minecraft/receipt.ts +340 -0
  100. package/src/worlds/minecraft/skill-context.ts +358 -0
  101. package/src/worlds/minecraft/skills-build.ts +1169 -0
  102. package/src/worlds/minecraft/skills-container.ts +1343 -0
  103. package/src/worlds/minecraft/skills-craft.ts +333 -0
  104. package/src/worlds/minecraft/skills-dig.ts +624 -0
  105. package/src/worlds/minecraft/skills-gather.ts +1230 -0
  106. package/src/worlds/minecraft/skills-interact.ts +1559 -0
  107. package/src/worlds/minecraft/tools.ts +331 -0
  108. package/src/worlds/minecraft/travel.ts +763 -0
  109. package/src/worlds/minecraft/until.ts +75 -0
  110. package/src/worlds/qq/normalize.ts +14 -0
  111. package/src/worlds/qq/world.ts +53 -7
  112. package/src/worlds/terminal/world.ts +5 -3
@@ -13,24 +13,13 @@ const zh = {
13
13
  settingsTitle: '设置',
14
14
  finishing: '正在关闭…',
15
15
  shutdownTitle: '关机',
16
- restartAria: '重启',
17
- restartTitleSupervised: '重启',
18
- restartTitleUnsupervised: '退出后需手动启动',
19
16
  shutdownBody: '进程将退出,控制台连接将断开。',
20
17
  confirmShutdownTitle: '⚠ 关机',
21
- confirmRestartTitle: '⚠ 重启',
22
- restartSupervisedNote: '进程将重启。',
23
- restartUnsupervisedNote: '进程将退出,需要手动重新启动。',
24
18
  confirmShutdownAgainTitle: '⚠⚠ 确认关机?',
25
19
  confirmShutdownAgainBody: '停止运行并退出进程?',
26
- confirmRestartAgainTitle: '⚠⚠ 确认重启?',
27
- confirmRestartAgainSupervised: '退出并重新启动进程?',
28
- confirmRestartAgainUnsupervised: '退出进程?退出后需手动启动。',
29
20
  finishingToast: '正在关闭…',
30
21
  stepIncomplete: '未完成',
31
22
  doneShutdown: '已关机',
32
- doneRestartSupervised: '已退出,等待重新启动',
33
- doneRestart: '已退出',
34
23
  resultLocalIncomplete: '本地关闭步骤未全部完成',
35
24
  resultExternalUnverified: '本地已关机,外部状态未确认',
36
25
  resultUnverified: '关闭步骤已执行,状态未确认',
@@ -56,24 +45,13 @@ const en: typeof zh = {
56
45
  settingsTitle: 'Settings',
57
46
  finishing: 'Shutting down…',
58
47
  shutdownTitle: 'Shut down',
59
- restartAria: 'Restart',
60
- restartTitleSupervised: 'Restart',
61
- restartTitleUnsupervised: 'Manual startup required after exit',
62
48
  shutdownBody: 'The process will exit and disconnect the console.',
63
49
  confirmShutdownTitle: '⚠ Shut down',
64
- confirmRestartTitle: '⚠ Restart',
65
- restartSupervisedNote: 'The process will restart.',
66
- restartUnsupervisedNote: 'The process will exit and must be restarted manually.',
67
50
  confirmShutdownAgainTitle: '⚠⚠ Confirm shutdown?',
68
51
  confirmShutdownAgainBody: 'Stop running and exit the process?',
69
- confirmRestartAgainTitle: '⚠⚠ Confirm restart?',
70
- confirmRestartAgainSupervised: 'Exit and restart the process?',
71
- confirmRestartAgainUnsupervised: 'Exit the process? You must start it manually afterwards.',
72
52
  finishingToast: 'Shutting down…',
73
53
  stepIncomplete: 'incomplete',
74
54
  doneShutdown: 'Shut down',
75
- doneRestartSupervised: 'Exited; waiting to restart',
76
- doneRestart: 'Exited',
77
55
  resultLocalIncomplete: 'Local shutdown steps incomplete',
78
56
  resultExternalUnverified: 'Shut down locally; external state unverified',
79
57
  resultUnverified: 'Shutdown steps executed; state unverified',
@@ -17,7 +17,9 @@ export type ConsoleIconName =
17
17
  | 'text'
18
18
  | 'eye'
19
19
  | 'eye-off'
20
- | 'refresh';
20
+ | 'pencil'
21
+ | 'refresh'
22
+ | 'trash';
21
23
 
22
24
  type Shape = readonly [tag: 'path' | 'circle' | 'rect' | 'line', attrs: Readonly<Record<string, string>>];
23
25
 
@@ -93,10 +95,21 @@ const SHAPES: Readonly<Record<ConsoleIconName, readonly Shape[]>> = {
93
95
  ['path', { d: 'M6.6 6.6C3.8 8.5 2 12 2 12s3.5 7 10 7c1.6 0 3-.4 4.3-1' }],
94
96
  ['path', { d: 'M9.9 9.9a3 3 0 0 0 4.2 4.2' }],
95
97
  ],
98
+ pencil: [
99
+ ['path', { d: 'M4 20h4.2L19.4 8.8a2.3 2.3 0 0 0-3.2-3.2L5 16.8V20Z' }],
100
+ ['path', { d: 'm12.6 6.5 4.9 4.9' }],
101
+ ],
96
102
  refresh: [
97
103
  ['path', { d: 'M21 12a9 9 0 1 1-2.6-6.4' }],
98
104
  ['path', { d: 'M21 3v6h-6' }],
99
105
  ],
106
+ trash: [
107
+ ['path', { d: 'M4 7h16' }],
108
+ ['path', { d: 'M9.5 7V5h5v2' }],
109
+ ['path', { d: 'M6.5 7v12a2 2 0 0 0 2 2h7a2 2 0 0 0 2-2V7' }],
110
+ ['path', { d: 'M10 11v6' }],
111
+ ['path', { d: 'M14 11v6' }],
112
+ ],
100
113
  };
101
114
 
102
115
  /**
@@ -44,7 +44,7 @@ const TEXTAREA_CLASS =
44
44
 
45
45
  /** 两枚圆形图标钮共用的底:尺寸、焦点环、禁用态。 */
46
46
  const ICON_BUTTON =
47
- 'inline-flex size-8 shrink-0 items-center justify-center rounded-full p-0 outline-none transition-[background-color,color,opacity,transform] duration-150 disabled:pointer-events-none disabled:opacity-40 focus-visible:ring-2 focus-visible:ring-ring/40 [&_svg]:pointer-events-none [&_svg]:size-4';
47
+ 'inline-flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full p-0 outline-none transition-[background-color,color,opacity,transform] duration-150 disabled:pointer-events-none disabled:opacity-40 focus-visible:ring-2 focus-visible:ring-ring/40 [&_svg]:pointer-events-none [&_svg]:size-4';
48
48
 
49
49
  /** 发送钮:实底,是这一栏唯一的主动作。 */
50
50
  const SEND_CLASS = `${ICON_BUTTON} self-end bg-foreground text-background hover:bg-foreground/90 active:translate-y-px focus-visible:ring-offset-2 focus-visible:ring-offset-background`;
@@ -124,6 +124,7 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
124
124
  const [placeholder, setPlaceholder] = React.useState<string | null>(null);
125
125
  const textarea = React.useRef<HTMLTextAreaElement>(null);
126
126
  const tools = React.useRef<HTMLSpanElement>(null);
127
+ const leading = React.useRef<HTMLSpanElement>(null);
127
128
  const picker = React.useRef<HTMLInputElement>(null);
128
129
  const nextId = React.useRef(1);
129
130
  const imagesOpts = opts.images;
@@ -137,6 +138,14 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
137
138
  return () => node.remove();
138
139
  }, [opts.tools]);
139
140
 
141
+ React.useLayoutEffect(() => {
142
+ const node = opts.leading;
143
+ const host = leading.current;
144
+ if (!node || !host) return;
145
+ host.appendChild(node);
146
+ return () => node.remove();
147
+ }, [opts.leading]);
148
+
140
149
  React.useImperativeHandle(ref, () => ({
141
150
  focus: () => textarea.current?.focus(),
142
151
  setDisabled,
@@ -237,10 +246,13 @@ const PromptComposer = React.forwardRef<ComposerHandle, { opts: ConsolePromptInp
237
246
  />
238
247
  </div>
239
248
  <div className="flex min-h-9 items-center justify-between gap-2 px-1">
240
- <span className="truncate px-2 text-xs text-muted-foreground">
241
- {note ? <span className="text-danger">{note}</span>
242
- : pending > 0 ? S.processingImages(pending)
243
- : opts.hint ?? S.enterToSend}
249
+ <span className="flex min-w-0 items-center gap-1">
250
+ <span ref={leading} className="flex items-center" />
251
+ <span className="truncate px-1 text-xs text-muted-foreground">
252
+ {note ? <span className="text-danger">{note}</span>
253
+ : pending > 0 ? S.processingImages(pending)
254
+ : opts.hint}
255
+ </span>
244
256
  </span>
245
257
  <div className="flex items-center gap-1">
246
258
  {imagesOpts && (
@@ -27,7 +27,6 @@ const zh = {
27
27
  messageInput: '消息输入',
28
28
  typeMessage: '输入消息…',
29
29
  processingImages: (count: number) => `处理 ${count} 张图片…`,
30
- enterToSend: 'Enter 发送 · Shift+Enter 换行',
31
30
  sendMessage: '发送消息',
32
31
  addImage: '添加图片',
33
32
  addImageHint: '添加图片(也可粘贴或拖入)',
@@ -61,7 +60,6 @@ const en: typeof zh = {
61
60
  messageInput: 'Message input',
62
61
  typeMessage: 'Type a message…',
63
62
  processingImages: (count: number) => `Processing ${count} image(s)…`,
64
- enterToSend: 'Enter to send · Shift+Enter for newline',
65
63
  sendMessage: 'Send message',
66
64
  addImage: 'Add image',
67
65
  addImageHint: 'Add image (paste or drop also works)',
@@ -318,7 +318,7 @@ export class ConsolePageRegistry {
318
318
  ): Promise<{ ok: true; value: unknown } | { ok: false; failure: InvokeFailure }> {
319
319
  const found = await this.resolvePanel(pageId, panelId, language);
320
320
  if (!found.ok) return found;
321
- // GET 只允许 getMethods 声明的方法。
321
+ // GET 不过写请求的同源闸门,任何站点凭 <img src> 就能发出;只放行面板点名的方法。
322
322
  if (transport === 'get' && !(found.panel.getMethods ?? []).includes(method)) {
323
323
  return {
324
324
  ok: false,
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 诊断包:一次导出把排查一场跑要看的记录收进一个 JSON —— 运行指纹、状态快照、常驻 session
3
+ * 的上下文、事件、运行日志、工具调用、交接边界、用量与脱敏配置。
4
+ *
5
+ * 每段各有条数上限,取到上限的段名列在 `truncated` 里;配置里键名含 secret / token / key /
6
+ * password 的值在写出前抹掉。缺席的接缝(未挂载调试通道、未挂载用量)那一段为空。
7
+ */
8
+
9
+ import { dirname, join } from 'node:path';
10
+ import { existsSync, readFileSync } from 'node:fs';
11
+ import type { ContextRecord } from '../protocol/open-responses/context.ts';
12
+ import type { EventEnvelope, LogRecord } from '../core/types.ts';
13
+ import type { SessionStats } from '../core/sessions.ts';
14
+ import { redactSecrets } from '../core/util.ts';
15
+ import { logPredicate, readRunsIndex, readTailRecordsWhere, type RunIndexRow } from './files.ts';
16
+
17
+ /**
18
+ * 每段取最近多少条,与 `/api/log` 的缺省条数一致。事件段非它不可:`store.range` 不给 limit
19
+ * 会把事件库整份读出来。取到这个数的段名进 `truncated`,拿到包的人据此知道还有更早的记录。
20
+ */
21
+ export const DIAGNOSTICS_TAIL = 200;
22
+
23
+ export interface DiagnosticsToolSchema {
24
+ name: string;
25
+ description: string;
26
+ parameters: Record<string, unknown>;
27
+ }
28
+
29
+ /** 活数据由 WebApp 现取交进来;落盘记录由本模块按 `dataDir` 与 `runId` 自己读。 */
30
+ export interface DiagnosticsSources {
31
+ dataDir: string;
32
+ runId: string | null;
33
+ exportedAt: Date;
34
+ status: Record<string, unknown>;
35
+ /** 常驻 session 的上下文与合成开头;没挂调试通道时为 null。 */
36
+ session: { messages: readonly ContextRecord[]; head: readonly ContextRecord[] } | null;
37
+ sessions: readonly SessionStats[];
38
+ toolSchemas: readonly DiagnosticsToolSchema[];
39
+ events: readonly EventEnvelope[];
40
+ latestCursor: number | null;
41
+ worlds: readonly unknown[];
42
+ /** `/api/usage` 那份聚合;没挂用量时为 null。 */
43
+ usage: unknown;
44
+ }
45
+
46
+ export interface DiagnosticsBundle {
47
+ kind: 'cortico-diagnostics';
48
+ version: 1;
49
+ exportedAt: string;
50
+ run: { id: string | null; index: RunIndexRow | null; manifest: unknown };
51
+ status: Record<string, unknown>;
52
+ worlds: readonly unknown[];
53
+ session: { messages: readonly ContextRecord[]; head: readonly ContextRecord[] } | null;
54
+ sessions: readonly SessionStats[];
55
+ toolSchemas: readonly DiagnosticsToolSchema[];
56
+ events: { latestCursor: number | null; items: readonly EventEnvelope[] };
57
+ /** `warn` 是 warn 及以上,`tail` 不论级别取最近几条。 */
58
+ log: { warn: LogRecord[]; tail: LogRecord[] };
59
+ toolcalls: unknown[];
60
+ /** transcript 里的 handoff / clear / prefix-reload 记号。 */
61
+ transcriptBoundaries: unknown[];
62
+ usage: { aggregate: unknown; rows: unknown[] };
63
+ config: unknown;
64
+ truncated: string[];
65
+ }
66
+
67
+ function readJson(file: string): unknown {
68
+ return existsSync(file) ? (JSON.parse(readFileSync(file, 'utf8')) as unknown) : null;
69
+ }
70
+
71
+ export function buildDiagnostics(sources: DiagnosticsSources): DiagnosticsBundle {
72
+ const { dataDir, runId } = sources;
73
+ const runDir = runId ? join(dataDir, 'runs', runId) : null;
74
+ const warn = runDir
75
+ ? readTailRecordsWhere<LogRecord>(join(runDir, 'log.jsonl'), DIAGNOSTICS_TAIL, logPredicate({ level: 'warn' }))
76
+ : [];
77
+ const tail = runDir
78
+ ? readTailRecordsWhere<LogRecord>(join(runDir, 'log.jsonl'), DIAGNOSTICS_TAIL, () => true)
79
+ : [];
80
+ const toolcalls = runDir
81
+ ? readTailRecordsWhere<unknown>(join(runDir, 'toolcalls.jsonl'), DIAGNOSTICS_TAIL, () => true)
82
+ : [];
83
+ const boundaries = runDir
84
+ ? readTailRecordsWhere<{ kind?: string }>(
85
+ join(runDir, 'transcript.jsonl'),
86
+ DIAGNOSTICS_TAIL,
87
+ (r) => r?.kind === 'boundary',
88
+ )
89
+ : [];
90
+ const usageRows = runId
91
+ ? readTailRecordsWhere<{ run?: string }>(
92
+ join(dataDir, 'usage.jsonl'),
93
+ DIAGNOSTICS_TAIL,
94
+ (r) => r?.run === runId,
95
+ )
96
+ : [];
97
+ const events = sources.events.slice(-DIAGNOSTICS_TAIL);
98
+
99
+ const truncated: string[] = [];
100
+ for (const [name, rows] of [
101
+ ['events', events],
102
+ ['log.warn', warn],
103
+ ['log.tail', tail],
104
+ ['toolcalls', toolcalls],
105
+ ['transcriptBoundaries', boundaries],
106
+ ['usage.rows', usageRows],
107
+ ] as const) {
108
+ if (rows.length >= DIAGNOSTICS_TAIL) truncated.push(name);
109
+ }
110
+
111
+ const index = runId
112
+ ? readRunsIndex(join(dataDir, 'runs', 'index.jsonl')).find((row) => row.run === runId) ?? null
113
+ : null;
114
+
115
+ return {
116
+ kind: 'cortico-diagnostics',
117
+ version: 1,
118
+ exportedAt: sources.exportedAt.toISOString(),
119
+ run: { id: runId, index, manifest: runDir ? readJson(join(runDir, 'run.json')) : null },
120
+ status: sources.status,
121
+ worlds: sources.worlds,
122
+ session: sources.session,
123
+ sessions: sources.sessions,
124
+ toolSchemas: sources.toolSchemas,
125
+ events: { latestCursor: sources.latestCursor, items: events },
126
+ log: { warn, tail },
127
+ toolcalls,
128
+ transcriptBoundaries: boundaries,
129
+ usage: { aggregate: sources.usage, rows: usageRows },
130
+ config: redactSecrets(readJson(join(dirname(dataDir), 'config.json'))),
131
+ truncated,
132
+ };
133
+ }
@@ -0,0 +1,67 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <title>登录 · Cortico</title>
7
+ <style>
8
+ :root { color-scheme: light dark; --bg: #f4f5f4; --card: #fff; --ink: #1d2321; --dim: #66706c; --line: #d5dad7; --accent: #2f7d62; --bad: #b3261e; }
9
+ @media (prefers-color-scheme: dark) {
10
+ :root { --bg: #141716; --card: #1d2120; --ink: #e6ebe8; --dim: #98a39e; --line: #333a37; --accent: #5fc29f; --bad: #f2867f; }
11
+ }
12
+ * { box-sizing: border-box; }
13
+ body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: var(--bg); color: var(--ink); font: 15px/1.5 system-ui, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
14
+ form { width: min(360px, calc(100vw - 32px)); padding: 28px; background: var(--card); border: 1px solid var(--line); border-radius: 12px; }
15
+ h1 { margin: 0 0 4px; font-size: 18px; font-weight: 600; }
16
+ p { margin: 0 0 20px; color: var(--dim); font-size: 13px; }
17
+ label { display: block; margin-bottom: 6px; font-size: 13px; color: var(--dim); }
18
+ input { width: 100%; padding: 9px 11px; font: inherit; color: inherit; background: var(--bg); border: 1px solid var(--line); border-radius: 8px; }
19
+ input:focus-visible, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
20
+ button { width: 100%; margin-top: 16px; padding: 9px 11px; font: inherit; font-weight: 600; color: var(--card); background: var(--accent); border: 0; border-radius: 8px; cursor: pointer; }
21
+ button:disabled { opacity: .6; cursor: default; }
22
+ #error { min-height: 20px; margin: 12px 0 0; color: var(--bad); font-size: 13px; }
23
+ </style>
24
+ </head>
25
+ <body>
26
+ <form id="login" autocomplete="on">
27
+ <h1 data-t="title"></h1>
28
+ <p data-t="hint"></p>
29
+ <label for="password" data-t="label"></label>
30
+ <input id="password" name="password" type="password" autocomplete="current-password" required autofocus>
31
+ <button type="submit" data-t="submit"></button>
32
+ <div id="error" role="alert"></div>
33
+ </form>
34
+ <script>
35
+ (() => {
36
+ const en = document.documentElement.lang === 'en';
37
+ const T = en
38
+ ? { title: 'Cortico console', hint: 'This console is protected by an access password.', label: 'Password', submit: 'Sign in', wrong: 'Wrong password.', failed: 'Sign-in failed: ' }
39
+ : { title: 'Cortico 控制台', hint: '这个控制台设置了访问密码。', label: '密码', submit: '登录', wrong: '密码不对。', failed: '登录失败:' };
40
+ document.title = (en ? 'Sign in' : '登录') + ' · Cortico';
41
+ for (const el of document.querySelectorAll('[data-t]')) el.textContent = T[el.dataset.t];
42
+ const form = document.getElementById('login');
43
+ const input = document.getElementById('password');
44
+ const button = form.querySelector('button');
45
+ const error = document.getElementById('error');
46
+ form.addEventListener('submit', async (event) => {
47
+ event.preventDefault();
48
+ button.disabled = true;
49
+ error.textContent = '';
50
+ try {
51
+ const res = await fetch('/api/auth/login', {
52
+ method: 'POST',
53
+ headers: { 'Content-Type': 'application/json' },
54
+ body: JSON.stringify({ password: input.value }),
55
+ });
56
+ if (res.ok) { location.reload(); return; }
57
+ error.textContent = res.status === 401 ? T.wrong : T.failed + 'HTTP ' + res.status;
58
+ } catch (err) {
59
+ error.textContent = T.failed + String(err);
60
+ }
61
+ button.disabled = false;
62
+ input.select();
63
+ });
64
+ })();
65
+ </script>
66
+ </body>
67
+ </html>
@@ -54,8 +54,8 @@
54
54
  --shadow-soft: color-mix(in srgb, var(--ink) 7%, transparent);
55
55
  --shadow-strong: color-mix(in srgb, var(--ink) 20%, transparent);
56
56
  --overlay: color-mix(in srgb, var(--ink) 44%, transparent);
57
- --sans: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
58
- --mono: ui-monospace, "Cascadia Mono", "Sarasa Mono SC", Consolas, "PingFang SC", "Microsoft YaHei", monospace;
57
+ --sans: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", "Noto Sans CJK SC", sans-serif;
58
+ --mono: ui-monospace, "Cascadia Mono", "Sarasa Mono SC", Consolas, monospace;
59
59
  --background: var(--paper);
60
60
  --surface: var(--sheet);
61
61
  --surface-hover: var(--sheet-2);
@@ -135,9 +135,21 @@
135
135
  .navdot.warn { background: var(--warn); }
136
136
  .navdot.bad { background: var(--danger); }
137
137
 
138
- .railfoot { padding: 10px 16px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--ink-dim); display: flex; flex-direction: column; align-items: stretch; gap: 8px; }
139
- .rail-who { display: flex; align-items: center; gap: 9px; min-width: 0; }
140
- .rail-name { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 13px; color: var(--ink); }
138
+ /* 头像、名字、那几个键在同一行:名字吃掉中间的余量,放不下就省略号。 */
139
+ .railfoot { padding: 10px 12px; border-top: 1px solid var(--line); font-size: 11.5px; color: var(--ink-dim); display: flex; align-items: center; gap: 8px; }
140
+ .rail-who { display: flex; align-items: center; gap: 9px; min-width: 0; flex: 1; }
141
+ .rail-name {
142
+ display: flex; align-items: center; gap: 5px; min-width: 0;
143
+ padding: 3px 6px; border: 0; border-radius: 7px; background: transparent;
144
+ font: 600 13px/1.35 var(--sans); color: var(--ink); cursor: pointer; text-align: left;
145
+ }
146
+ .rail-name-text { min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
147
+ /* 铅笔平时不占视线,悬停/聚焦才露出来——名字可点这件事只在那时才需要说。 */
148
+ .rail-name-edit { display: grid; place-items: center; flex: none; opacity: 0; transition: opacity .15s ease; }
149
+ .rail-name-edit .icon { width: 13px; height: 13px; }
150
+ .rail-name:hover { background: var(--sheet-2); }
151
+ .rail-name:hover .rail-name-edit, .rail-name:focus-visible .rail-name-edit { opacity: .75; }
152
+ .rail-rename { flex: 1; min-width: 0; }
141
153
 
142
154
 
143
155
  .pagehead {
@@ -147,7 +159,7 @@
147
159
  .pagedesc { font-size: 13px; color: var(--ink-soft); max-width: 68ch; }
148
160
 
149
161
  .scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 30px 40px; }
150
- .measure { max-width: 1080px; }
162
+ .measure { max-width: var(--content-width); }
151
163
 
152
164
 
153
165
  .subtabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 12px 30px 0; flex: none; }
@@ -209,12 +221,14 @@
209
221
  }
210
222
  .btn:hover { color: var(--ink); border-color: var(--line-strong); background: var(--sheet); }
211
223
  .btn:disabled { opacity: .45; cursor: default; }
212
- .btn.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
213
- .btn.primary:hover { filter: brightness(1.06); color: var(--on-accent); }
224
+ .btn.primary { background: var(--accent); color: #fff; border-color: var(--accent); }
225
+ .btn.primary:hover { filter: brightness(1.06); color: #fff; }
214
226
  .btn.warn:hover { color: var(--warn); border-color: var(--warn); }
215
227
  .btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
216
228
  .btn.danger:hover { background: color-mix(in srgb, var(--danger) 9%, transparent); border-color: var(--danger); }
217
229
  .btn.sm { padding: 4px 11px; font-size: var(--text-detail); }
230
+ .btn-ico { display: inline-flex; align-items: center; gap: 6px; }
231
+ .btn-ico .icon { width: 14px; height: 14px; }
218
232
 
219
233
  .msgline { font: var(--text-detail)/1.45 var(--sans); min-height: 16px; color: var(--ok); }
220
234
  .msgline:empty { display: none; }
@@ -627,7 +641,7 @@
627
641
  min-height: 36px;
628
642
  transition: background-color .15s ease, color .15s ease, border-color .15s ease, opacity .15s ease;
629
643
  }
630
- .btn.primary, .seg.active { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
644
+ .btn.primary, .seg.active { background: var(--accent); border-color: var(--accent); color: #fff; }
631
645
  input.field, select.field, textarea.field { min-height: 40px; }
632
646
  input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible, [tabindex]:focus-visible {
633
647
  outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px;
@@ -887,7 +901,7 @@
887
901
  background: color-mix(in srgb, var(--border) 84%, transparent);
888
902
  }
889
903
  .navdot { width: 6px; height: 6px; }
890
- .railfoot { min-height: 62px; padding: 10px 16px 14px; border: 0; gap: 9px; }
904
+ .railfoot { min-height: 62px; padding: 10px 12px 14px; border: 0; gap: 8px; }
891
905
  .rail-avatar-wrap {
892
906
  display: grid;
893
907
  flex: none;
@@ -924,11 +938,12 @@
924
938
  }
925
939
  .rail-avatar-edit .icon { width: 15px; height: 15px; }
926
940
  .rail-avatar:hover .rail-avatar-edit { opacity: 1; }
927
- .rail-actions { display: flex; gap: 3px; }
928
- .rail-name { color: var(--foreground); }
941
+ .rail-actions { display: flex; flex: none; gap: 2px; }
942
+ .rail-name { color: var(--foreground); font: 600 13.5px/1.35 var(--sans); }
943
+ .rail-name:hover { background: var(--hover); }
929
944
  .rail-action {
930
- width: 34px;
931
- height: 34px;
945
+ width: 30px;
946
+ height: 30px;
932
947
  display: grid;
933
948
  place-items: center;
934
949
  padding: 0;
@@ -976,17 +991,19 @@
976
991
  overflow-x: hidden;
977
992
  overflow-y: auto;
978
993
  padding: 30px var(--page-gutter) 56px;
979
- scrollbar-gutter: stable;
994
+ scrollbar-gutter: stable both-edges;
980
995
  }
981
996
  .providerchrome {
997
+ overflow: hidden;
998
+ scrollbar-gutter: stable both-edges;
982
999
  flex: none;
983
1000
  min-width: 0;
984
1001
  padding: 30px var(--page-gutter) 0;
985
1002
  }
986
1003
  .panelslot { padding-top: 8px; }
987
- .featureslot > :not(.viewport-overlay),
1004
+ .featureslot > :where(:not(.viewport-overlay)),
988
1005
  .providerchrome > *,
989
- .panelslot > :not(.viewport-overlay) {
1006
+ .panelslot > :where(:not(.viewport-overlay)) {
990
1007
  flex: none;
991
1008
  width: 100%;
992
1009
  max-width: var(--content-width);
@@ -999,9 +1016,8 @@
999
1016
  padding: 0;
1000
1017
  }
1001
1018
  .coreview .measure {
1002
- width: min(100%, var(--conversation-width));
1003
- margin-right: auto;
1004
- margin-left: 0;
1019
+ width: min(100%, var(--content-width));
1020
+ margin-inline: auto;
1005
1021
  }
1006
1022
  .featureslot-live > *,
1007
1023
  .featureslot-core > * {
@@ -1022,7 +1038,7 @@
1022
1038
  .providerintro .msgline { margin-top: 10px; }
1023
1039
  .pagetitle { margin-top: 7px; font: 600 22px/1.3 var(--sans); letter-spacing: -.025em; }
1024
1040
  .pagedesc { margin-top: 5px; color: var(--muted-foreground); font-size: 13px; line-height: 1.55; }
1025
- .scroll { padding: 22px var(--page-gutter) 48px; scrollbar-gutter: stable; }
1041
+ .scroll { padding: 22px var(--page-gutter) 48px; scrollbar-gutter: stable both-edges; }
1026
1042
  .measure { width: min(100%, var(--content-width)); margin-inline: auto; }
1027
1043
  .subtabs {
1028
1044
  width: 100%;
@@ -1055,6 +1071,11 @@
1055
1071
  details.sheet.fold { margin-bottom: 10px; }
1056
1072
  details.sheet.fold > summary { padding: 0; }
1057
1073
  details.sheet.fold > .foldbody { padding: 14px 0 0; }
1074
+ /* 整张点开详细页的卡(npm 搜索结果)。取值与 .connection-card 一致。 */
1075
+ .iocard-open { cursor: pointer; transition: background .15s, border-color .15s; }
1076
+ .iocard-open:hover { border-color: var(--border-strong); background: color-mix(in srgb, var(--surface) 92%, transparent); }
1077
+ .iocard-open:focus-visible { outline: 2px solid var(--ink-dim); outline-offset: 1px; }
1078
+ .iocard-open.is-installed { border-color: var(--accent); box-shadow: 0 0 8px color-mix(in srgb, var(--accent) 12%, transparent); }
1058
1079
  .featureslot > :not(.sheet) + .sheet,
1059
1080
  .featureslot > .sheet + .sheet,
1060
1081
  .panelslot > :not(.sheet) + .sheet,
@@ -1137,7 +1158,7 @@
1137
1158
  }
1138
1159
  .prompt-toolbar .msgline { min-width: 0; overflow-wrap: anywhere; }
1139
1160
  .featureslot-prompts > .prompt-library,
1140
- .panelslot > .prompt-library { margin-top: 28px; }
1161
+ .panelslot > .prompt-library { margin-top: 0; }
1141
1162
  .prompt-card textarea.field {
1142
1163
  width: 100%;
1143
1164
  min-height: 300px;
@@ -1419,7 +1440,7 @@
1419
1440
  box-shadow: none;
1420
1441
  }
1421
1442
  .btn:hover { background: var(--hover); color: var(--foreground); border-color: transparent; box-shadow: none; }
1422
- .btn.primary { background: var(--accent); border-color: var(--accent); color: var(--on-accent); }
1443
+ .btn.primary { background: var(--accent); border-color: var(--accent); color: #fff; }
1423
1444
  .btn.primary:hover { background: color-mix(in srgb, var(--accent) 90%, var(--ink)); filter: none; }
1424
1445
  :root[data-theme-scheme="mint"] .btn.primary {
1425
1446
  background: color-mix(in srgb, var(--accent) 78%, var(--sheet-2));
@@ -1439,6 +1460,15 @@
1439
1460
  font-family: var(--sans);
1440
1461
  font-size: 13px;
1441
1462
  }
1463
+ select.field:not([multiple]):not([size]) {
1464
+ appearance: none;
1465
+ padding-right: 36px;
1466
+ background-image: linear-gradient(45deg, transparent 50%, var(--muted-foreground) 50%),
1467
+ linear-gradient(135deg, var(--muted-foreground) 50%, transparent 50%);
1468
+ background-position: right 18px center, right 14px center;
1469
+ background-size: 4px 4px;
1470
+ background-repeat: no-repeat;
1471
+ }
1442
1472
  input.field:focus, select.field:focus, textarea.field:focus { border-color: var(--border-strong); }
1443
1473
  .fieldrow .fieldlabel { margin-bottom: 6px; color: var(--muted-foreground); font-size: 11px; letter-spacing: 0; text-transform: none; }
1444
1474
  .segwrap { gap: 2px; padding: 2px; background: var(--surface-hover); border: 0; border-radius: var(--radius-md); }
@@ -1577,7 +1607,7 @@
1577
1607
  .providerhub .panelslot { display: block; flex: none; padding: 0; overflow: visible; container: none; }
1578
1608
  .providerhub .panelslot { margin-top: 22px; }
1579
1609
  .providerhub .providerchrome > *,
1580
- .providerhub .panelslot > :not(.viewport-overlay) { max-width: none; }
1610
+ .providerhub .panelslot > :where(:not(.viewport-overlay)) { max-width: none; }
1581
1611
  .providerhub .providerintro .pagetitle { margin: 0; font-size: 18px; }
1582
1612
  .providerhub .providerintro .pagedesc { margin-top: 4px; }
1583
1613
  .providerhub .providerintro .rowbar { margin-top: 10px; }
@@ -1608,7 +1638,7 @@
1608
1638
 
1609
1639
  @media (max-width: 900px) {
1610
1640
  #rail { width: 232px; }
1611
- .pagehead, .scroll, .subtabs, .liveband, .tlscroll { padding-left: 20px; padding-right: 20px; }
1641
+ :root { --page-gutter: 20px; }
1612
1642
  .settings-layout { grid-template-columns: minmax(0, 1fr); gap: 20px; }
1613
1643
  .settings-index { position: static; flex-direction: row; overflow-x: auto; }
1614
1644
  .settings-jump { width: auto; white-space: nowrap; }
@@ -1711,5 +1741,92 @@
1711
1741
  .ob-btn:hover:not(:disabled) { background: var(--hover); }
1712
1742
  .ob-btn .icon { width: 14px; height: 14px; }
1713
1743
  .ob-btn:disabled { opacity: .55; cursor: not-allowed; }
1714
- .ob-btn.ob-go { background: var(--accent); color: var(--on-accent); }
1744
+ .ob-btn.ob-go { background: var(--accent); color: #fff; }
1715
1745
  .ob-btn.ob-go:hover:not(:disabled) { filter: brightness(1.06); }
1746
+ .connection-hub{display:grid;grid-template-columns:minmax(210px,280px) minmax(0,1fr);gap:24px;align-items:start}
1747
+ .connection-index{position:sticky;top:16px;min-width:0;max-height:calc(100vh - 140px);overflow:auto;padding:4px}
1748
+ .connection-cards{display:grid;gap:12px;padding:3px}
1749
+ .connection-card{min-width:0;border:1px solid var(--line);border-radius:12px;padding:14px;background:var(--paper);cursor:pointer;transition:background .15s,border-color .15s}
1750
+ .connection-card.is-active{border-color:var(--accent);box-shadow:0 0 8px color-mix(in srgb,var(--accent) 12%,transparent)}
1751
+ .connection-card.is-selected{background:color-mix(in srgb,var(--ink) 5%,var(--paper));outline:2px solid var(--ink-dim);outline-offset:1px}
1752
+ .connection-name,.connection-kind,.connection-model,.connection-url,.connection-status,.connection-secondary{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1753
+ .connection-name{font:700 17px/1.25 var(--sans)}.connection-kind{margin-top:4px;font:600 11.5px/1.3 var(--sans);color:var(--muted-foreground)}.connection-model{margin-top:9px;font-size:13px}.connection-url{font-size:11px;color:var(--ink-dim);margin-top:3px}
1754
+ .connection-status{font-size:12px;margin-top:14px}.connection-secondary{font-size:11px;color:var(--ink-dim)}
1755
+ .connection-card .rowbar{margin-top:12px;justify-content:space-between;gap:4px}
1756
+ .connection-detail{min-width:0}.connection-detail .sheet{margin-bottom:16px}
1757
+ .connection-detail .connection-field{margin-bottom:15px}.connection-detail input:not([type=checkbox]),.connection-detail select,.connection-detail textarea{max-width:100%;width:100%}
1758
+ .connection-detail .field-error{color:var(--danger,#b43c38);font-size:12px;min-height:0}.connection-shared{color:var(--ink-dim);font-size:12px;line-height:1.7;margin:20px 0 12px}
1759
+ .connection-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.connection-actions .grow{flex:1}
1760
+ @media(max-width:760px){.connection-hub{grid-template-columns:1fr}.connection-index{position:static;max-height:320px}.connection-cards{grid-template-columns:repeat(auto-fit,minmax(210px,1fr))}}
1761
+ .connection-guard{border:1px solid var(--line);border-radius:16px;padding:24px;background:var(--paper);color:var(--ink);max-width:560px;box-shadow:0 12px 60px #0003}
1762
+ .connection-guard::backdrop{background:#0005}.connection-guard h3{margin-bottom:20px}
1763
+
1764
+ .live-summary { flex-wrap:wrap; }
1765
+ .live-connection { display:inline-flex; align-items:center; gap:7px; min-width:0; max-width:48%; background:none; border:0; color:var(--ink-soft); cursor:pointer; padding:0; font:11.5px/1 var(--sans); }
1766
+ .live-connection-label { flex-shrink:0; }
1767
+ .live-connection-value { min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; padding:4px 8px; border-radius:7px; background:var(--hover); border:1px solid var(--border); color:var(--ink-blue); font-weight:600; }
1768
+ .live-connection:hover .live-connection-value { border-color:var(--accent); }
1769
+ .live-connection:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:7px; }
1770
+ @media(max-width:760px) { .live-connection { max-width:100%; margin-left:auto; } }
1771
+
1772
+ .connection-detail .msgline:not(.bad) { color:var(--ink-soft); }
1773
+ .connection-detail .tdesc { margin:6px 0 12px; }
1774
+
1775
+ /* Page headings stay above the shared content origin while each page scrolls. */
1776
+ .featureslot:has(> .featureintro) { padding-top:0; }
1777
+ .featureintro { padding-bottom:16px; border-bottom:1px solid var(--border); }
1778
+ .featureslot > .featureintro { position:sticky; top:0; z-index:20; padding-top:30px; margin-bottom:24px; background:var(--background); }
1779
+ .featureslot > .featureintro + * { margin-top:0; }
1780
+ .featureslot > .featureintro + .sheet { margin-top:0; }
1781
+ .providerchrome { padding-bottom:0; }
1782
+ .providerchrome + .panelslot { padding-top:24px; }
1783
+ .coreview > .pagehead { padding-bottom:16px; }
1784
+ .connection-index { padding-top:0; top:100px; }
1785
+ .connection-detail > div > div:first-child > .sheet:first-child { padding-top:0; }
1786
+ .connection-hub .btn { border:1px solid var(--border-strong); }
1787
+ .connection-hub .btn:hover { border-color:var(--ink-soft); }
1788
+ .connection-hub .btn.danger { border-color:currentColor; }
1789
+ .connection-hub .btn.primary, .connection-hub .btn.primary:hover { color:#fff; border-color:var(--accent); }
1790
+ .connection-module-readonly:disabled { opacity:1; background-color:var(--surface); color:var(--foreground); cursor:default; }
1791
+ .field-note, .tdesc, .trow .tdesc, .sheet .sh-desc, .pagedesc, p.muted, small.muted,
1792
+ .connection-detail .msgline:not(.bad), .connection-shared {
1793
+ font:var(--text-detail)/1.55 var(--sans); color:var(--muted-foreground);
1794
+ }
1795
+ .field-note { margin:10px 0; }
1796
+ .field-note:empty { display:none; }
1797
+ .pricing-rules, .pricing-snapshot { margin-top:16px; }
1798
+ .pricing-rules > textarea, .pricing-snapshot > pre { margin-top:10px; }
1799
+
1800
+ .coreview > .pagehead { position:relative; border-bottom:0; }
1801
+ .coreview > .pagehead::after { content:''; position:absolute; bottom:0; left:var(--page-gutter); right:var(--page-gutter); height:1px; background:var(--border); }
1802
+ .coreview > .subtabs { margin-top:24px; }
1803
+
1804
+ .providerchrome > .featureintro + * { margin-top: 24px; }
1805
+ .providerchrome > .pagedesc { margin-bottom: 10px; }
1806
+ .providerchrome > .rowbar { margin-bottom: 10px; }
1807
+ .coreview > .scroll { scrollbar-gutter: auto; }
1808
+ .panelslot > :first-child > .sheet:first-child { padding-top: 0; }
1809
+
1810
+ /* 卡片上的删除与探活。 */
1811
+ .connection-create { margin-bottom:16px; }
1812
+ .connection-create-hint { margin:7px 2px 0; font:var(--text-detail)/1.5 var(--sans); color:var(--muted-foreground); }
1813
+ .connection-create > .btn { width:100%; }
1814
+ .connection-card { position:relative; }
1815
+ .connection-erase { position:absolute; top:10px; right:10px; display:flex; align-items:center; gap:0; height:26px; padding:0 6px; border:1px solid transparent; border-radius:8px; background:none; color:var(--ink-dim); cursor:pointer; transition:color .14s ease, background .14s ease, border-color .14s ease, gap .18s ease; }
1816
+ .connection-erase .icon { width:14px; height:14px; flex:none; }
1817
+ .connection-erase:hover { color:var(--danger,#b43c38); background:var(--hover); }
1818
+ .connection-erase-text { display:grid; grid-template-columns:0fr; overflow:hidden; font:600 12px/1 var(--sans); white-space:nowrap; transition:grid-template-columns .18s ease; }
1819
+ .connection-erase-text > span { overflow:hidden; }
1820
+ .connection-erase.is-armed { gap:6px; color:var(--danger,#b43c38); border-color:currentColor; background:var(--paper); }
1821
+ .connection-erase.is-armed .connection-erase-text { grid-template-columns:1fr; }
1822
+ .connection-name { padding-right:34px; }
1823
+ .connection-probe-btn .connection-spin { display:none; width:12px; height:12px; margin-left:6px; border:2px solid currentColor; border-right-color:transparent; border-radius:50%; animation:connection-spin .7s linear infinite; }
1824
+ .connection-probe-btn.is-busy .connection-spin { display:inline-block; }
1825
+ @keyframes connection-spin { to { transform:rotate(360deg); } }
1826
+ .connection-probe { display:grid; grid-template-rows:0fr; margin-top:0; transition:grid-template-rows .2s ease, margin-top .2s ease, opacity .2s ease; opacity:0; }
1827
+ .connection-probe.is-open { grid-template-rows:1fr; margin-top:12px; opacity:1; }
1828
+ .connection-probe-body { overflow:hidden; }
1829
+ .connection-probe .sheet { margin:0; padding:10px 12px; border:1px solid var(--border); border-radius:10px; background:var(--surface); }
1830
+ .connection-probe .kvtable td { padding:3px 0; font-size:11.5px; word-break:break-word; }
1831
+ .connection-probe .kvtable td:first-child { padding-right:8px; color:var(--muted-foreground); white-space:nowrap; }
1832
+ .connection-probe > .connection-probe-body > .btn { margin-top:8px; }