dsh-ntr 0.2.1 → 0.3.1
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.
- package/README.en.md +4 -4
- package/README.md +4 -4
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +646 -68
- package/lib/client/panel.js +162 -4
- package/lib/client/store.js +188 -4
- package/lib/client.js +431 -9
- package/lib/client.js.map +1 -1
- package/lib/impl.js +47 -0
- package/lib/index.js +15 -1
- package/lib/types/bridge.d.ts +206 -4
- package/lib/types/client/store.d.ts +87 -1
- package/lib/types/impl.d.ts +26 -1
- package/lib/types/protocol.d.ts +80 -0
- package/lib/types/update.d.ts +186 -0
- package/lib/update.js +696 -0
- package/package.json +1 -1
- package/src/bridge.ts +691 -68
- package/src/client/panel.tsx +288 -5
- package/src/client/store.ts +234 -8
- package/src/impl.ts +66 -1
- package/src/index.ts +16 -1
- package/src/protocol.ts +84 -0
- package/src/update.ts +712 -0
package/src/client/panel.tsx
CHANGED
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { useEffect, useRef, useState, useSyncExternalStore } from 'react'
|
|
19
19
|
import { formatCommandLine, isAbsolutePathLike, splitCommandLine } from './acp-ui.tsx'
|
|
20
|
-
import { DEFAULT_KNOWN_PRESETS } from './store.ts'
|
|
20
|
+
import { DEFAULT_KNOWN_PRESETS, LOG_MAX_ENTRIES } from './store.ts'
|
|
21
21
|
import type { AcpStore, UiState } from './store.ts'
|
|
22
|
-
import type { AcpAgentDef, AcpApprovalDeadlines, AcpCatalogChoice, AcpMcpServerDef, AgentSavePayload, AgentView, DetectedAgentInfo, PermissionTimeoutMode } from '../protocol.ts'
|
|
22
|
+
import type { AcpAgentDef, AcpApprovalDeadlines, AcpCatalogChoice, AcpMcpServerDef, AgentSavePayload, AgentView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState } from '../protocol.ts'
|
|
23
23
|
|
|
24
24
|
interface WithStore { acp: AcpStore }
|
|
25
25
|
|
|
@@ -83,7 +83,7 @@ const STYLE = `
|
|
|
83
83
|
}
|
|
84
84
|
.dwa-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
|
|
85
85
|
.dwa-page-heading { min-width: 0; }
|
|
86
|
-
.dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; }
|
|
86
|
+
.dwa-page-title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 550; letter-spacing: -0.02em; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
|
|
87
87
|
.dwa-page-description { margin: 5px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-sm); }
|
|
88
88
|
.dwa-btn {
|
|
89
89
|
min-height: 32px; box-sizing: border-box; font: inherit; border-radius: var(--ntr-radius-sm);
|
|
@@ -115,6 +115,20 @@ const STYLE = `
|
|
|
115
115
|
.dwa-badge-success { background: var(--dsw-alias-state-success-tertiary, #dcfce7); color: var(--ntr-success); }
|
|
116
116
|
.dwa-badge-warn { background: var(--dsw-alias-state-warn-tertiary, #fef3c7); color: var(--ntr-warn); }
|
|
117
117
|
.dwa-badge-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
118
|
+
/* The update badge is a button (it expands the update card): strip the
|
|
119
|
+
browser's default button chrome, keep the badge look. */
|
|
120
|
+
.dwa-update-badge { border: 0; font-family: inherit; cursor: pointer; }
|
|
121
|
+
.dwa-update-badge:hover { opacity: 0.85; }
|
|
122
|
+
/* The version badge doubles as the update entry: same badge look, no chrome. */
|
|
123
|
+
.dwa-version-btn { border: 0; font-family: inherit; cursor: pointer; }
|
|
124
|
+
.dwa-version-btn:hover { opacity: 0.85; }
|
|
125
|
+
.dwa-version { font-family: var(--ds-font-family-code, monospace); }
|
|
126
|
+
/* The update card sits between the page header and the permission mirror. */
|
|
127
|
+
.dwa-update-card { margin-bottom: 16px; }
|
|
128
|
+
.dwa-update-summary { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; font-weight: 550; }
|
|
129
|
+
.dwa-update-copy { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); }
|
|
130
|
+
.dwa-update-message { margin: 6px 0 0; color: var(--ntr-muted); font-size: var(--ntr-font-xs); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
131
|
+
.dwa-update-actions { margin-top: 10px; }
|
|
118
132
|
.dwa-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
|
|
119
133
|
.dwa-agent-list { display: flex; flex-direction: column; border-top: var(--ntr-line); }
|
|
120
134
|
.dwa-agent-card { border-bottom: var(--ntr-line); min-width: 0; }
|
|
@@ -237,6 +251,17 @@ const STYLE = `
|
|
|
237
251
|
.dwa-deadline-flag { margin-top: 4px; font-size: var(--ntr-font-xs); font-weight: 550; color: var(--ntr-warn); }
|
|
238
252
|
.dwa-deadline-warn { margin-top: 10px; padding: 9px 12px; border-left: 3px solid var(--dsw-alias-state-warn-primary, #d97706); background: var(--dsw-alias-state-warn-secondary, #fffbeb); border-radius: var(--ntr-radius-sm); font-size: var(--ntr-font-xs); line-height: 1.55; }
|
|
239
253
|
.dwa-deadline-reset { display: flex; justify-content: flex-end; margin-top: 8px; }
|
|
254
|
+
/* Runtime log: one bounded list, newest first. The time column never wraps;
|
|
255
|
+
the message is the only cell that may break anywhere. */
|
|
256
|
+
.dwa-log-list { list-style: none; margin: 0; padding: 0; border: var(--ntr-line); border-radius: var(--ntr-radius); overflow: hidden; }
|
|
257
|
+
.dwa-log-row { display: flex; align-items: baseline; gap: 8px; padding: 6px 10px; font-size: var(--ntr-font-sm); border-top: var(--ntr-line); min-width: 0; }
|
|
258
|
+
.dwa-log-row:first-child { border-top: 0; }
|
|
259
|
+
.dwa-log-row.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); }
|
|
260
|
+
.dwa-log-time { color: var(--ntr-muted); white-space: nowrap; }
|
|
261
|
+
.dwa-log-text { flex: 1; min-width: 0; overflow-wrap: anywhere; }
|
|
262
|
+
.dwa-log-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--ntr-danger); display: inline-block; margin-left: 6px; }
|
|
263
|
+
.dwa-log-level.is-info { background: var(--dsw-alias-state-business-tertiary, #dbeafe); color: var(--dsw-alias-state-business-primary, #2563eb); }
|
|
264
|
+
.dwa-log-level.is-error { background: var(--dsw-alias-state-error-tertiary, #fef2f2); color: var(--ntr-danger); }
|
|
240
265
|
@media (max-width: 640px) {
|
|
241
266
|
.dwa-root { padding: 20px 16px; }
|
|
242
267
|
}
|
|
@@ -852,7 +877,170 @@ function ApprovalDeadlinesCard(props: { acp: AcpStore; ui: UiState }) {
|
|
|
852
877
|
)
|
|
853
878
|
}
|
|
854
879
|
|
|
855
|
-
type SettingsPage = 'agents' | 'add' | 'approvals' | 'appearance' | 'diagnostics'
|
|
880
|
+
type SettingsPage = 'agents' | 'add' | 'approvals' | 'appearance' | 'logs' | 'diagnostics'
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Local wall-clock `HH:MM:SS` for one runtime-log line. Hand-built on purpose:
|
|
884
|
+
* `toLocaleTimeString` may drop seconds or pick a 12-hour clock depending on
|
|
885
|
+
* the browser locale, which makes the column ragged and ambiguous.
|
|
886
|
+
*/
|
|
887
|
+
function logClock(at: number): string {
|
|
888
|
+
const date = new Date(at)
|
|
889
|
+
return [date.getHours(), date.getMinutes(), date.getSeconds()]
|
|
890
|
+
.map(value => String(value).padStart(2, '0'))
|
|
891
|
+
.join(':')
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/** The states that carry an update badge next to the version badge. */
|
|
895
|
+
function updateBadgeText(update: PluginUpdateState, acp: AcpStore): string | undefined {
|
|
896
|
+
if (update.status === 'available') {
|
|
897
|
+
return acp.label(`可更新 v${update.latest ?? update.current}`, `Update v${update.latest ?? update.current}`)
|
|
898
|
+
}
|
|
899
|
+
if (update.status === 'applying') return acp.label('更新中…', 'Updating…')
|
|
900
|
+
if (update.status === 'applied') {
|
|
901
|
+
const installed = update.installed ?? update.latest ?? update.current
|
|
902
|
+
return acp.label(`已更新 v${installed}`, `Updated v${installed}`)
|
|
903
|
+
}
|
|
904
|
+
if (update.status === 'restart-required') {
|
|
905
|
+
// Only a KNOWN disk version can be named. An interrupted install leaves
|
|
906
|
+
// `installed` unset, and a badge would have to invent a version.
|
|
907
|
+
if (update.installed === undefined) return undefined
|
|
908
|
+
// Neutral: the disk version can be newer OR older than the running one.
|
|
909
|
+
return acp.label(`待重启 v${update.installed}`, `Restart for v${update.installed}`)
|
|
910
|
+
}
|
|
911
|
+
return undefined
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* The version badge's update companion. Every badge is a button, `applying`
|
|
916
|
+
* included: the card is where the install's outcome is read, so the running
|
|
917
|
+
* install must be able to open it too. States without anything to act on get
|
|
918
|
+
* no badge at all — the version badge itself still opens the card.
|
|
919
|
+
*/
|
|
920
|
+
function PluginUpdateBadge(props: { acp: AcpStore; update: PluginUpdateState; open: boolean; onToggle: () => void }) {
|
|
921
|
+
const { acp, update, open, onToggle } = props
|
|
922
|
+
const text = updateBadgeText(update, acp)
|
|
923
|
+
if (text === undefined) return null
|
|
924
|
+
return (
|
|
925
|
+
<button
|
|
926
|
+
type="button"
|
|
927
|
+
className="dwa-badge dwa-badge-info dwa-update-badge"
|
|
928
|
+
aria-expanded={open}
|
|
929
|
+
aria-controls="ntr-plugin-update-card"
|
|
930
|
+
onClick={onToggle}
|
|
931
|
+
>
|
|
932
|
+
{text}
|
|
933
|
+
</button>
|
|
934
|
+
)
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* The update card. In-page, no dialog: the settings page already owns the one
|
|
939
|
+
* action (install the exact version the host reported), and a modal on top of
|
|
940
|
+
* a settings section would hide the version it is updating.
|
|
941
|
+
*
|
|
942
|
+
* The copy stays to one line per state (constraint 33); the host's raw error
|
|
943
|
+
* (`message`) is the only long text, and it wraps on its own line so a stack
|
|
944
|
+
* trace or a registry error is readable without stretching the card.
|
|
945
|
+
*/
|
|
946
|
+
function PluginUpdateCard(props: { acp: AcpStore; update: PluginUpdateState; onClose: () => void }) {
|
|
947
|
+
const { acp, update, onClose } = props
|
|
948
|
+
const applying = update.status === 'applying'
|
|
949
|
+
const latest = update.latest ?? update.current
|
|
950
|
+
const onDisk = update.installed !== undefined && update.installed !== update.current
|
|
951
|
+
? update.installed
|
|
952
|
+
: undefined
|
|
953
|
+
const published = update.publishedAt !== undefined && !Number.isNaN(new Date(update.publishedAt).getTime())
|
|
954
|
+
? new Date(update.publishedAt).toLocaleDateString()
|
|
955
|
+
: undefined
|
|
956
|
+
const copy = update.status === 'available'
|
|
957
|
+
? acp.label('更新会卸载并重装插件,之后可能需要重启 DSH。', 'Updating uninstalls and reinstalls the plugin, and may require restarting DSH.')
|
|
958
|
+
: update.status === 'applied'
|
|
959
|
+
? acp.label('已应用,刷新页面即可使用新版本。', 'Applied. Reload the page to use the new version.')
|
|
960
|
+
: update.status === 'restart-required'
|
|
961
|
+
? update.installed !== undefined
|
|
962
|
+
? acp.label(
|
|
963
|
+
`磁盘已是 v${update.installed},重启 DSH 后生效(桌面 App 退出重开;命令行重启 dsh web)。`,
|
|
964
|
+
`On disk v${update.installed}. Restart DSH to apply (quit and reopen the app, or restart dsh web).`,
|
|
965
|
+
)
|
|
966
|
+
: acp.label(
|
|
967
|
+
'更新可能已完成,请刷新页面确认当前版本。',
|
|
968
|
+
'The update may already be complete. Refresh the page to confirm the current version.',
|
|
969
|
+
)
|
|
970
|
+
: update.status === 'busy'
|
|
971
|
+
? acp.label('有进行中的对话,结束后才能更新。', 'Finish the running conversation before updating.')
|
|
972
|
+
: update.status === 'failed'
|
|
973
|
+
? acp.label('更新失败。', 'Update failed.')
|
|
974
|
+
: update.status === 'current'
|
|
975
|
+
? acp.label('已是最新版本。', 'Up to date.')
|
|
976
|
+
: update.status === 'source-install'
|
|
977
|
+
? acp.label(
|
|
978
|
+
'源码或本地安装,无法通过 registry 更新。',
|
|
979
|
+
'Source or local install; update it through its own checkout.',
|
|
980
|
+
)
|
|
981
|
+
: update.status === 'managed'
|
|
982
|
+
? acp.label('此安装由宿主管理,不能在这里更新。', 'This install is managed by the host and cannot be updated here.')
|
|
983
|
+
: undefined
|
|
984
|
+
const showMessage = (update.status === 'failed' || update.status === 'busy' || update.status === 'managed')
|
|
985
|
+
&& update.message !== undefined
|
|
986
|
+
const apply = async (): Promise<void> => {
|
|
987
|
+
if (update.latest === undefined) return
|
|
988
|
+
try {
|
|
989
|
+
await acp.applyPluginUpdate(update.latest)
|
|
990
|
+
} catch {
|
|
991
|
+
// The store keeps the "disk may have changed" state and writes its own notice.
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
return (
|
|
995
|
+
<div
|
|
996
|
+
id="ntr-plugin-update-card"
|
|
997
|
+
className="dwa-card dwa-update-card"
|
|
998
|
+
role="group"
|
|
999
|
+
aria-label={acp.label('插件更新', 'Plugin update')}
|
|
1000
|
+
>
|
|
1001
|
+
<div className="dwa-update-summary">
|
|
1002
|
+
<span>{acp.label(`当前 v${update.current}`, `Current v${update.current}`)}</span>
|
|
1003
|
+
{onDisk !== undefined && <span className="dwa-hint">{acp.label(`磁盘 v${onDisk}`, `On disk v${onDisk}`)}</span>}
|
|
1004
|
+
{update.latest !== undefined && (
|
|
1005
|
+
<>
|
|
1006
|
+
<span aria-hidden="true">→</span>
|
|
1007
|
+
<span>{acp.label(`最新 v${latest}`, `Latest v${latest}`)}</span>
|
|
1008
|
+
{published !== undefined && <span className="dwa-hint">{published}</span>}
|
|
1009
|
+
</>
|
|
1010
|
+
)}
|
|
1011
|
+
</div>
|
|
1012
|
+
{copy !== undefined && <p className="dwa-update-copy">{copy}</p>}
|
|
1013
|
+
{showMessage && <div className="dwa-update-message">{update.message}</div>}
|
|
1014
|
+
<div className="dwa-row-actions dwa-update-actions">
|
|
1015
|
+
{update.status === 'available' && (
|
|
1016
|
+
<button type="button" className="dwa-btn dwa-btn-primary" onClick={() => { void apply() }}>
|
|
1017
|
+
{acp.label('更新', 'Update')}
|
|
1018
|
+
</button>
|
|
1019
|
+
)}
|
|
1020
|
+
{applying && (
|
|
1021
|
+
<button type="button" className="dwa-btn dwa-btn-primary" disabled>
|
|
1022
|
+
{acp.label('正在更新…', 'Updating…')}
|
|
1023
|
+
</button>
|
|
1024
|
+
)}
|
|
1025
|
+
{update.status !== 'applying' && (
|
|
1026
|
+
<button type="button" className="dwa-btn" onClick={onClose}>{acp.label('关闭', 'Close')}</button>
|
|
1027
|
+
)}
|
|
1028
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" disabled={applying} onClick={() => void acp.checkPluginUpdate(true)}>
|
|
1029
|
+
{acp.label('重新检查', 'Check again')}
|
|
1030
|
+
</button>
|
|
1031
|
+
</div>
|
|
1032
|
+
</div>
|
|
1033
|
+
)
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* The card is drawn for every state a host can report about this install,
|
|
1038
|
+
* except the two where there is no install to talk about at all.
|
|
1039
|
+
*/
|
|
1040
|
+
function updateCardVisible(update: PluginUpdateState | undefined, open: boolean): boolean {
|
|
1041
|
+
if (update === undefined || !open) return false
|
|
1042
|
+
return update.status !== 'unsupported' && update.status !== 'not-installed'
|
|
1043
|
+
}
|
|
856
1044
|
|
|
857
1045
|
export function AcpSettingsSection(props: WithStore) {
|
|
858
1046
|
const acp = props.acp
|
|
@@ -862,6 +1050,17 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
862
1050
|
const [page, setPage] = useState<SettingsPage>('agents')
|
|
863
1051
|
const [addOpen, setAddOpen] = useState(false)
|
|
864
1052
|
const [addTarget, setAddTarget] = useState<DetectedAgentInfo | undefined>(undefined)
|
|
1053
|
+
const [updateOpen, setUpdateOpen] = useState(false)
|
|
1054
|
+
// The store throttles this to one registry read per ten minutes, so every
|
|
1055
|
+
// remount (both settings entries mount this section) is cheap.
|
|
1056
|
+
useEffect(() => {
|
|
1057
|
+
void acp.checkPluginUpdate()
|
|
1058
|
+
}, [acp])
|
|
1059
|
+
// Opening the log is the read receipt. The store does not mutate at zero,
|
|
1060
|
+
// so this cannot loop.
|
|
1061
|
+
useEffect(() => {
|
|
1062
|
+
if (page === 'logs' && ui.logUnread > 0) acp.markLogsRead()
|
|
1063
|
+
}, [acp, page, ui.logUnread])
|
|
865
1064
|
const { configured, available } = partitionAgents(ui.detectedAgents, ui.agents)
|
|
866
1065
|
const selected = addTarget ?? available[0] ?? NEW_AGENT_SEED
|
|
867
1066
|
const openAdd = (): void => {
|
|
@@ -869,12 +1068,49 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
869
1068
|
setPage('add')
|
|
870
1069
|
}
|
|
871
1070
|
const presetProblem = ui.preset !== undefined && (!ui.preset.installed || ui.preset.problem !== undefined)
|
|
1071
|
+
const update = ui.pluginUpdate
|
|
1072
|
+
// A source install cannot be replaced by this browser; the version badge
|
|
1073
|
+
// says so on hover. Neutral wording: a checkout may be git or not.
|
|
1074
|
+
const versionTitle = update?.status === 'source-install'
|
|
1075
|
+
? acp.label('源码或本地安装,无法通过 registry 更新。', 'Source or local install; update it through its own checkout.')
|
|
1076
|
+
: undefined
|
|
1077
|
+
// The running version is the update entry: every state with a real install
|
|
1078
|
+
// report can open the card, including "up to date" (which offers a re-check).
|
|
1079
|
+
const versionOpensUpdate = update !== undefined
|
|
1080
|
+
&& update.status !== 'unsupported'
|
|
1081
|
+
&& update.status !== 'not-installed'
|
|
872
1082
|
|
|
873
1083
|
return (
|
|
874
1084
|
<div className="dwa-root">
|
|
875
1085
|
<header className="dwa-page-header">
|
|
876
1086
|
<div className="dwa-page-heading">
|
|
877
|
-
<h2 className="dwa-page-title"
|
|
1087
|
+
<h2 className="dwa-page-title" aria-label={acp.label('NTR 设置', 'NTR settings')}>
|
|
1088
|
+
{acp.label('NTR 设置', 'NTR settings')}
|
|
1089
|
+
{ui.pluginVersion !== undefined && (
|
|
1090
|
+
versionOpensUpdate
|
|
1091
|
+
? (
|
|
1092
|
+
<button
|
|
1093
|
+
type="button"
|
|
1094
|
+
className="dwa-badge dwa-version dwa-version-btn"
|
|
1095
|
+
title={versionTitle}
|
|
1096
|
+
aria-expanded={updateOpen}
|
|
1097
|
+
aria-controls="ntr-plugin-update-card"
|
|
1098
|
+
onClick={() => setUpdateOpen(open => !open)}
|
|
1099
|
+
>
|
|
1100
|
+
{`v${ui.pluginVersion}`}
|
|
1101
|
+
</button>
|
|
1102
|
+
)
|
|
1103
|
+
: <span className="dwa-badge dwa-version" title={versionTitle}>{`v${ui.pluginVersion}`}</span>
|
|
1104
|
+
)}
|
|
1105
|
+
{update !== undefined && (
|
|
1106
|
+
<PluginUpdateBadge
|
|
1107
|
+
acp={acp}
|
|
1108
|
+
update={update}
|
|
1109
|
+
open={updateOpen}
|
|
1110
|
+
onToggle={() => setUpdateOpen(open => !open)}
|
|
1111
|
+
/>
|
|
1112
|
+
)}
|
|
1113
|
+
</h2>
|
|
878
1114
|
<p className="dwa-page-description">
|
|
879
1115
|
{acp.label('连接智能体,设置你的对话偏好。', 'Connect agents and set your conversation preferences.')}
|
|
880
1116
|
</p>
|
|
@@ -886,6 +1122,10 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
886
1122
|
)}
|
|
887
1123
|
</header>
|
|
888
1124
|
|
|
1125
|
+
{update !== undefined && updateCardVisible(update, updateOpen) && (
|
|
1126
|
+
<PluginUpdateCard acp={acp} update={update} onClose={() => setUpdateOpen(false)} />
|
|
1127
|
+
)}
|
|
1128
|
+
|
|
889
1129
|
{/* The cross-session mirror: the conversation dock answers the asks
|
|
890
1130
|
belonging to the open conversation; this card answers the rest. */}
|
|
891
1131
|
{ledger.visible && (
|
|
@@ -942,6 +1182,10 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
942
1182
|
<button type="button" aria-pressed={page === 'agents'} onClick={() => setPage('agents')}>{acp.label('智能体', 'Agents')}</button>
|
|
943
1183
|
{ui.approvalDeadlines !== undefined && <button type="button" aria-pressed={page === 'approvals'} onClick={() => setPage('approvals')}>{acp.label('审批时限', 'Approval deadlines')}</button>}
|
|
944
1184
|
<button type="button" aria-pressed={page === 'appearance'} onClick={() => setPage('appearance')}>{acp.label('对话外观', 'Appearance')}</button>
|
|
1185
|
+
<button type="button" aria-pressed={page === 'logs'} onClick={() => setPage('logs')}>
|
|
1186
|
+
{acp.label('日志', 'Logs')}
|
|
1187
|
+
{ui.logUnread > 0 && <span className="dwa-log-dot" aria-hidden="true" />}
|
|
1188
|
+
</button>
|
|
945
1189
|
</nav>
|
|
946
1190
|
{page === 'diagnostics' && (
|
|
947
1191
|
<section className="dwa-section">
|
|
@@ -1122,6 +1366,45 @@ export function AcpSettingsSection(props: WithStore) {
|
|
|
1122
1366
|
)}
|
|
1123
1367
|
</div>
|
|
1124
1368
|
</section>
|
|
1369
|
+
{/* Conditional on purpose: this list can hold 200 rows, and keeping it
|
|
1370
|
+
mounted through a `hidden` page would re-render every line on each
|
|
1371
|
+
`session/update`. It carries no local state, so remounting is free. */}
|
|
1372
|
+
{page === 'logs' && (
|
|
1373
|
+
<section className="dwa-section" aria-label={acp.label('运行日志', 'Runtime log')}>
|
|
1374
|
+
<div className="dwa-section-heading">
|
|
1375
|
+
<div>
|
|
1376
|
+
<h3 className="dwa-section-title">{acp.label('运行日志', 'Runtime log')}</h3>
|
|
1377
|
+
<p className="dwa-section-copy">
|
|
1378
|
+
{acp.label(
|
|
1379
|
+
`agent 运行消息,最多 ${String(LOG_MAX_ENTRIES)} 条。`,
|
|
1380
|
+
`Agent runtime messages, up to ${String(LOG_MAX_ENTRIES)}.`,
|
|
1381
|
+
)}
|
|
1382
|
+
</p>
|
|
1383
|
+
</div>
|
|
1384
|
+
{ui.logs.length > 0 && (
|
|
1385
|
+
<button type="button" className="dwa-btn dwa-btn-quiet" onClick={() => acp.clearLogs()}>
|
|
1386
|
+
{acp.label('清空', 'Clear')}
|
|
1387
|
+
</button>
|
|
1388
|
+
)}
|
|
1389
|
+
</div>
|
|
1390
|
+
{ui.logs.length === 0
|
|
1391
|
+
? <div className="dwa-empty" role="status">{acp.label('暂无运行日志。', 'No runtime messages yet.')}</div>
|
|
1392
|
+
: (
|
|
1393
|
+
<ul className="dwa-log-list">
|
|
1394
|
+
{[...ui.logs].reverse().map(entry => (
|
|
1395
|
+
<li key={entry.id} className={entry.level === 'error' ? 'dwa-log-row is-error' : 'dwa-log-row'}>
|
|
1396
|
+
<span className="dwa-log-time dwa-mono">{logClock(entry.at)}</span>
|
|
1397
|
+
<span className={`dwa-badge dwa-log-level ${entry.level === 'error' ? 'is-error' : 'is-info'}`}>
|
|
1398
|
+
{entry.level === 'error' ? acp.label('错误', 'Error') : acp.label('信息', 'Info')}
|
|
1399
|
+
</span>
|
|
1400
|
+
{entry.agentId !== undefined && <span className="dwa-badge">{entry.agentId}</span>}
|
|
1401
|
+
<span className="dwa-log-text">{entry.text}</span>
|
|
1402
|
+
</li>
|
|
1403
|
+
))}
|
|
1404
|
+
</ul>
|
|
1405
|
+
)}
|
|
1406
|
+
</section>
|
|
1407
|
+
)}
|
|
1125
1408
|
<footer className="dwa-health">
|
|
1126
1409
|
<div className="dwa-health-status">
|
|
1127
1410
|
<span className="dwa-dot" style={{ background: ui.stream === 'live' ? 'var(--ntr-success)' : ui.stream === 'down' ? 'var(--ntr-danger)' : 'var(--ntr-muted)' }} />
|