cortico 0.1.1 → 0.1.3

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 (100) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/src/boot.ts +29 -0
  4. package/src/bot.ts +40 -5
  5. package/src/core/README.md +12 -2
  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 +18 -6
  10. package/src/core/generation.ts +66 -9
  11. package/src/core/instance-lock.ts +6 -0
  12. package/src/core/loop.ts +62 -32
  13. package/src/core/secrets.ts +2 -4
  14. package/src/core/session.ts +3 -2
  15. package/src/core/timers.ts +8 -6
  16. package/src/core/types.ts +23 -4
  17. package/src/core/util.ts +15 -0
  18. package/src/deploy.ts +5 -4
  19. package/src/extensions/README.md +6 -4
  20. package/src/extensions/dry-mount.ts +34 -6
  21. package/src/extensions/manifest.ts +21 -11
  22. package/src/extensions.ts +147 -23
  23. package/src/launcher.ts +30 -22
  24. package/src/protocol/open-responses/context-helpers.ts +3 -1
  25. package/src/protocol/open-responses/context-log.ts +37 -9
  26. package/src/providers/README.md +44 -12
  27. package/src/providers/base.ts +2 -0
  28. package/src/providers/console/config.ts +36 -0
  29. package/src/providers/console/hub.ts +307 -0
  30. package/src/providers/console/settings.ts +30 -26
  31. package/src/providers/console/strings.ts +14 -0
  32. package/src/providers/console/types.ts +5 -0
  33. package/src/providers/hub-api.ts +3 -0
  34. package/src/providers/llamacpp/config.ts +38 -0
  35. package/src/providers/llamacpp/console/models-panel.ts +87 -4
  36. package/src/providers/llamacpp/console/runtime-panel.ts +36 -73
  37. package/src/providers/llamacpp/console/server.ts +47 -4
  38. package/src/providers/llamacpp/huggingface.ts +93 -0
  39. package/src/providers/llamacpp/index.ts +5 -1
  40. package/src/providers/llamacpp/native.ts +10 -5
  41. package/src/providers/llamacpp/options.ts +2 -0
  42. package/src/providers/llamacpp/strings.ts +42 -0
  43. package/src/providers/name.ts +13 -0
  44. package/src/providers/openai-responses-compat/config.ts +33 -0
  45. package/src/providers/openai-responses-compat/console/client.ts +5 -0
  46. package/src/providers/openai-responses-compat/console/reasoning-panel.ts +84 -0
  47. package/src/providers/openai-responses-compat/console/server.ts +135 -0
  48. package/src/providers/openai-responses-compat/index.ts +40 -12
  49. package/src/providers/openai-responses-compat/native.ts +15 -5
  50. package/src/providers/openai-responses-compat/strings.ts +62 -1
  51. package/src/providers/pricebook.ts +5 -5
  52. package/src/providers/registry.ts +24 -10
  53. package/src/providers/strings.ts +2 -0
  54. package/src/providers/transport/errors.ts +9 -0
  55. package/src/providers/transport/response-http.ts +7 -3
  56. package/src/providers/transport/responses-input.ts +59 -10
  57. package/src/web/README.md +15 -3
  58. package/src/web/auth.ts +80 -0
  59. package/src/web/client/console-pages/builtins/llm-settings/panel.ts +42 -35
  60. package/src/web/client/console-pages/builtins/llm-settings/pricing-panel.ts +22 -8
  61. package/src/web/client/console-pages/builtins/llm-settings/strings.ts +2 -4
  62. package/src/web/client/console-pages/host.ts +22 -5
  63. package/src/web/client/core/api.ts +5 -1
  64. package/src/web/client/core/router.ts +15 -0
  65. package/src/web/client/features/extensions/index.ts +276 -41
  66. package/src/web/client/features/extensions/strings.ts +84 -10
  67. package/src/web/client/features/feature.ts +1 -1
  68. package/src/web/client/features/live/diagnostics.ts +32 -0
  69. package/src/web/client/features/live/index.ts +27 -11
  70. package/src/web/client/features/live/protocol.ts +1 -0
  71. package/src/web/client/features/live/strings.ts +9 -3
  72. package/src/web/client/features/providers/detail.ts +309 -0
  73. package/src/web/client/features/providers/drafts.ts +23 -0
  74. package/src/web/client/features/providers/index.ts +179 -87
  75. package/src/web/client/features/providers/strings.ts +48 -17
  76. package/src/web/client/features/providers/types.ts +15 -0
  77. package/src/web/client/features/settings/general.ts +12 -0
  78. package/src/web/client/features/settings/strings.ts +6 -0
  79. package/src/web/client/main.ts +4 -0
  80. package/src/web/client/shell/index.ts +1 -1
  81. package/src/web/client/ui/fields.ts +3 -1
  82. package/src/web/client/ui/icons.ts +9 -1
  83. package/src/web/client/ui/prompt-input.tsx +17 -5
  84. package/src/web/client/ui/strings.ts +0 -2
  85. package/src/web/diagnostics.ts +133 -0
  86. package/src/web/public/login.html +67 -0
  87. package/src/web/public/styles.css +130 -18
  88. package/src/web/server.ts +229 -21
  89. package/src/web/shared/client-panel.ts +3 -0
  90. package/src/web/shared/console-protocol.ts +16 -1
  91. package/src/worlds/bilibili/README.md +1 -1
  92. package/src/worlds/bilibili/overlay/server.ts +4 -2
  93. package/src/worlds/minecraft/ADAPT.md +66 -0
  94. package/src/worlds/minecraft/README.md +8 -0
  95. package/src/worlds/minecraft/client-launch.ts +11 -2
  96. package/src/worlds/minecraft/client.ts +4 -0
  97. package/src/worlds/minecraft/mineflayer-fixes.ts +55 -1
  98. package/src/worlds/qq/normalize.ts +14 -0
  99. package/src/worlds/qq/world.ts +56 -10
  100. package/src/worlds/terminal/world.ts +3 -4
@@ -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);
@@ -159,7 +159,7 @@
159
159
  .pagedesc { font-size: 13px; color: var(--ink-soft); max-width: 68ch; }
160
160
 
161
161
  .scroll { flex: 1; min-height: 0; overflow-y: auto; padding: 18px 30px 40px; }
162
- .measure { max-width: 1080px; }
162
+ .measure { max-width: var(--content-width); }
163
163
 
164
164
 
165
165
  .subtabs { display: flex; gap: 4px; flex-wrap: wrap; padding: 12px 30px 0; flex: none; }
@@ -221,12 +221,14 @@
221
221
  }
222
222
  .btn:hover { color: var(--ink); border-color: var(--line-strong); background: var(--sheet); }
223
223
  .btn:disabled { opacity: .45; cursor: default; }
224
- .btn.primary { background: var(--accent); color: var(--on-accent); border-color: var(--accent); }
225
- .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; }
226
226
  .btn.warn:hover { color: var(--warn); border-color: var(--warn); }
227
227
  .btn.danger { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
228
228
  .btn.danger:hover { background: color-mix(in srgb, var(--danger) 9%, transparent); border-color: var(--danger); }
229
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; }
230
232
 
231
233
  .msgline { font: var(--text-detail)/1.45 var(--sans); min-height: 16px; color: var(--ok); }
232
234
  .msgline:empty { display: none; }
@@ -639,7 +641,7 @@
639
641
  min-height: 36px;
640
642
  transition: background-color .15s ease, color .15s ease, border-color .15s ease, opacity .15s ease;
641
643
  }
642
- .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; }
643
645
  input.field, select.field, textarea.field { min-height: 40px; }
644
646
  input:focus-visible, textarea:focus-visible, select:focus-visible, button:focus-visible, [tabindex]:focus-visible {
645
647
  outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 2px;
@@ -989,17 +991,19 @@
989
991
  overflow-x: hidden;
990
992
  overflow-y: auto;
991
993
  padding: 30px var(--page-gutter) 56px;
992
- scrollbar-gutter: stable;
994
+ scrollbar-gutter: stable both-edges;
993
995
  }
994
996
  .providerchrome {
997
+ overflow: hidden;
998
+ scrollbar-gutter: stable both-edges;
995
999
  flex: none;
996
1000
  min-width: 0;
997
1001
  padding: 30px var(--page-gutter) 0;
998
1002
  }
999
1003
  .panelslot { padding-top: 8px; }
1000
- .featureslot > :not(.viewport-overlay),
1004
+ .featureslot > :where(:not(.viewport-overlay)),
1001
1005
  .providerchrome > *,
1002
- .panelslot > :not(.viewport-overlay) {
1006
+ .panelslot > :where(:not(.viewport-overlay)) {
1003
1007
  flex: none;
1004
1008
  width: 100%;
1005
1009
  max-width: var(--content-width);
@@ -1012,9 +1016,8 @@
1012
1016
  padding: 0;
1013
1017
  }
1014
1018
  .coreview .measure {
1015
- width: min(100%, var(--conversation-width));
1016
- margin-right: auto;
1017
- margin-left: 0;
1019
+ width: min(100%, var(--content-width));
1020
+ margin-inline: auto;
1018
1021
  }
1019
1022
  .featureslot-live > *,
1020
1023
  .featureslot-core > * {
@@ -1035,7 +1038,7 @@
1035
1038
  .providerintro .msgline { margin-top: 10px; }
1036
1039
  .pagetitle { margin-top: 7px; font: 600 22px/1.3 var(--sans); letter-spacing: -.025em; }
1037
1040
  .pagedesc { margin-top: 5px; color: var(--muted-foreground); font-size: 13px; line-height: 1.55; }
1038
- .scroll { padding: 22px var(--page-gutter) 48px; scrollbar-gutter: stable; }
1041
+ .scroll { padding: 22px var(--page-gutter) 48px; scrollbar-gutter: stable both-edges; }
1039
1042
  .measure { width: min(100%, var(--content-width)); margin-inline: auto; }
1040
1043
  .subtabs {
1041
1044
  width: 100%;
@@ -1068,6 +1071,11 @@
1068
1071
  details.sheet.fold { margin-bottom: 10px; }
1069
1072
  details.sheet.fold > summary { padding: 0; }
1070
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); }
1071
1079
  .featureslot > :not(.sheet) + .sheet,
1072
1080
  .featureslot > .sheet + .sheet,
1073
1081
  .panelslot > :not(.sheet) + .sheet,
@@ -1150,7 +1158,7 @@
1150
1158
  }
1151
1159
  .prompt-toolbar .msgline { min-width: 0; overflow-wrap: anywhere; }
1152
1160
  .featureslot-prompts > .prompt-library,
1153
- .panelslot > .prompt-library { margin-top: 28px; }
1161
+ .panelslot > .prompt-library { margin-top: 0; }
1154
1162
  .prompt-card textarea.field {
1155
1163
  width: 100%;
1156
1164
  min-height: 300px;
@@ -1432,7 +1440,7 @@
1432
1440
  box-shadow: none;
1433
1441
  }
1434
1442
  .btn:hover { background: var(--hover); color: var(--foreground); border-color: transparent; box-shadow: none; }
1435
- .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; }
1436
1444
  .btn.primary:hover { background: color-mix(in srgb, var(--accent) 90%, var(--ink)); filter: none; }
1437
1445
  :root[data-theme-scheme="mint"] .btn.primary {
1438
1446
  background: color-mix(in srgb, var(--accent) 78%, var(--sheet-2));
@@ -1452,6 +1460,15 @@
1452
1460
  font-family: var(--sans);
1453
1461
  font-size: 13px;
1454
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
+ }
1455
1472
  input.field:focus, select.field:focus, textarea.field:focus { border-color: var(--border-strong); }
1456
1473
  .fieldrow .fieldlabel { margin-bottom: 6px; color: var(--muted-foreground); font-size: 11px; letter-spacing: 0; text-transform: none; }
1457
1474
  .segwrap { gap: 2px; padding: 2px; background: var(--surface-hover); border: 0; border-radius: var(--radius-md); }
@@ -1590,7 +1607,7 @@
1590
1607
  .providerhub .panelslot { display: block; flex: none; padding: 0; overflow: visible; container: none; }
1591
1608
  .providerhub .panelslot { margin-top: 22px; }
1592
1609
  .providerhub .providerchrome > *,
1593
- .providerhub .panelslot > :not(.viewport-overlay) { max-width: none; }
1610
+ .providerhub .panelslot > :where(:not(.viewport-overlay)) { max-width: none; }
1594
1611
  .providerhub .providerintro .pagetitle { margin: 0; font-size: 18px; }
1595
1612
  .providerhub .providerintro .pagedesc { margin-top: 4px; }
1596
1613
  .providerhub .providerintro .rowbar { margin-top: 10px; }
@@ -1621,7 +1638,7 @@
1621
1638
 
1622
1639
  @media (max-width: 900px) {
1623
1640
  #rail { width: 232px; }
1624
- .pagehead, .scroll, .subtabs, .liveband, .tlscroll { padding-left: 20px; padding-right: 20px; }
1641
+ :root { --page-gutter: 20px; }
1625
1642
  .settings-layout { grid-template-columns: minmax(0, 1fr); gap: 20px; }
1626
1643
  .settings-index { position: static; flex-direction: row; overflow-x: auto; }
1627
1644
  .settings-jump { width: auto; white-space: nowrap; }
@@ -1724,5 +1741,100 @@
1724
1741
  .ob-btn:hover:not(:disabled) { background: var(--hover); }
1725
1742
  .ob-btn .icon { width: 14px; height: 14px; }
1726
1743
  .ob-btn:disabled { opacity: .55; cursor: not-allowed; }
1727
- .ob-btn.ob-go { background: var(--accent); color: var(--on-accent); }
1744
+ .ob-btn.ob-go { background: var(--accent); color: #fff; }
1728
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-status,.connection-secondary{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
1753
+ .connection-name{font:700 17px/1.25 var(--sans)}
1754
+ .connection-facts{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px;min-width:0}
1755
+ .connection-facts .kv{max-width:100%}.connection-facts .kv-v{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
1756
+ .kv-kind{--kv-line:color-mix(in srgb,var(--ink-blue) 60%,transparent)}.kv-model{--kv-line:color-mix(in srgb,var(--accent-2) 75%,transparent)}.kv-baseUrl{--kv-line:var(--line-strong)}
1757
+ .connection-status{font-size:12px;margin-top:14px}.connection-secondary{font-size:11px;color:var(--ink-dim)}
1758
+ .connection-detail{min-width:0}.connection-detail .sheet{margin-bottom:16px}
1759
+ .connection-identity{background:var(--paper-2);border-color:var(--line-2)}
1760
+ .connection-identity+.connection-flow{margin-top:22px;padding-top:18px;border-top:1px dashed var(--line-2)}
1761
+ .connection-flow{counter-reset:step}.connection-step{counter-increment:step;min-width:0}
1762
+ .connection-step td.mono{white-space:normal;overflow-wrap:anywhere}
1763
+ .connection-step table.data .btn{white-space:nowrap}
1764
+ .connection-step>.sheet>h3::before,.connection-step>details.sheet>summary>h3::before{content:counter(step);display:inline-grid;place-items:center;width:20px;height:20px;margin-right:9px;border-radius:50%;background:color-mix(in srgb,var(--accent) 16%,transparent);color:var(--accent);font:600 11px/1 var(--mono);vertical-align:2px}
1765
+ .connection-detail .connection-field{margin-bottom:15px}.connection-detail input:not([type=checkbox]),.connection-detail select,.connection-detail textarea{max-width:100%;width:100%}
1766
+ .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}
1767
+ .connection-actions{display:flex;gap:8px;flex-wrap:wrap;align-items:center}.connection-actions .grow{flex:1}
1768
+ @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))}}
1769
+
1770
+ .live-summary { flex-wrap:wrap; }
1771
+ .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); }
1772
+ .live-connection-label { flex-shrink:0; }
1773
+ .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; }
1774
+ .live-connection:hover .live-connection-value { border-color:var(--accent); }
1775
+ .live-connection:focus-visible { outline:2px solid var(--accent); outline-offset:3px; border-radius:7px; }
1776
+ @media(max-width:760px) { .live-connection { max-width:100%; margin-left:auto; } }
1777
+
1778
+ .connection-detail .msgline:not(.bad) { color:var(--ink-soft); }
1779
+ .connection-detail .tdesc { margin:6px 0 12px; }
1780
+
1781
+ /* Page headings stay above the shared content origin while each page scrolls. */
1782
+ .featureslot:has(> .featureintro) { padding-top:0; }
1783
+ .featureintro { padding-bottom:16px; border-bottom:1px solid var(--border); }
1784
+ .featureslot > .featureintro { position:sticky; top:0; z-index:20; padding-top:30px; margin-bottom:24px; background:var(--background); }
1785
+ .featureslot > .featureintro + * { margin-top:0; }
1786
+ .featureslot > .featureintro + .sheet { margin-top:0; }
1787
+ .providerchrome { padding-bottom:0; }
1788
+ .providerchrome + .panelslot { padding-top:24px; }
1789
+ .coreview > .pagehead { padding-bottom:16px; }
1790
+ .connection-index { padding-top:0; top:100px; }
1791
+ .connection-detail > div > div:first-child > .sheet:first-child { padding-top:0; }
1792
+ .connection-hub .btn { border:1px solid var(--border-strong); }
1793
+ .connection-hub .btn:hover { border-color:var(--ink-soft); }
1794
+ .connection-hub .btn.danger { border-color:currentColor; }
1795
+ .connection-hub .btn.primary, .connection-hub .btn.primary:hover { color:#fff; border-color:var(--accent); }
1796
+ .connection-module-readonly:disabled { opacity:1; background-color:var(--surface); color:var(--foreground); cursor:default; }
1797
+ .field-note, .tdesc, .trow .tdesc, .sheet .sh-desc, .pagedesc, p.muted, small.muted,
1798
+ .connection-detail .msgline:not(.bad), .connection-shared {
1799
+ font:var(--text-detail)/1.55 var(--sans); color:var(--muted-foreground);
1800
+ }
1801
+ .field-note { margin:10px 0; }
1802
+ .field-note:empty { display:none; }
1803
+ .pricing-rules, .pricing-snapshot { margin-top:16px; }
1804
+ .pricing-rules > textarea, .pricing-snapshot > pre { margin-top:10px; }
1805
+
1806
+ .coreview > .pagehead { position:relative; border-bottom:0; }
1807
+ .coreview > .pagehead::after { content:''; position:absolute; bottom:0; left:var(--page-gutter); right:var(--page-gutter); height:1px; background:var(--border); }
1808
+ .coreview > .subtabs { margin-top:24px; }
1809
+
1810
+ .providerchrome > .featureintro + * { margin-top: 24px; }
1811
+ .providerchrome > .pagedesc { margin-bottom: 10px; }
1812
+ .providerchrome > .rowbar { margin-bottom: 10px; }
1813
+ .coreview > .scroll { scrollbar-gutter: auto; }
1814
+ .panelslot > :first-child > .sheet:first-child { padding-top: 0; }
1815
+
1816
+ /* 卡片:连接在右上,删除在右下,图标底边对齐状态行。 */
1817
+ .connection-create { margin-bottom:16px; }
1818
+ .connection-create-hint { margin:7px 2px 0; font:var(--text-detail)/1.5 var(--sans); color:var(--muted-foreground); }
1819
+ .connection-create > .btn { width:100%; }
1820
+ .connection-card { position:relative; padding-right:64px; }
1821
+ .connection-erase { position:absolute; bottom:13px; right:13px; display:flex; align-items:center; gap:0; height:28px; padding:0 4px; 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; }
1822
+ .connection-erase .icon { width:20px; height:20px; flex:none; }
1823
+ .connection-erase:hover { color:var(--danger,#b43c38); background:var(--hover); }
1824
+ .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; }
1825
+ .connection-erase-text > span { overflow:hidden; }
1826
+ .connection-erase.is-armed { gap:6px; color:var(--danger,#b43c38); border-color:currentColor; background:var(--paper); }
1827
+ .connection-erase.is-armed .connection-erase-text { grid-template-columns:1fr; }
1828
+ .connection-connect { position:absolute; top:12px; right:12px; width:32px; text-align:center; }
1829
+ .connection-connect[hidden] { display:none; }
1830
+ .connection-hub .btn.connection-activate { display:block; width:32px; height:28px; min-height:28px; padding:0; border:0; font-size:17px; line-height:28px; }
1831
+ .connection-connect-label { display:block; font-size:10px; line-height:14px; color:var(--muted-foreground); }
1832
+ .connection-index { scrollbar-width:none; }
1833
+ .connection-index::-webkit-scrollbar { display:none; }
1834
+ .required-mark { color:var(--danger); }
1835
+ .connection-test { gap:12px; align-items:center; }
1836
+ .connection-test .msgline { margin:0; }
1837
+ .connection-status[data-tone="active"] { color:var(--accent); }
1838
+ .connection-status[data-tone="ready"] { color:var(--ok); }
1839
+ .connection-status[data-tone="error"], .connection-secondary[data-tone="error"] { color:var(--danger); }
1840
+ .connection-status[data-tone="draft"], .connection-secondary[data-tone="draft"] { color:var(--foreground); }