dsh-code 1.0.6 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.en.md +123 -26
  2. package/README.md +124 -27
  3. package/bin/deepseek.mjs +283 -35
  4. package/cordis.patch.yml +97 -0
  5. package/lib/index.mjs +5008 -881
  6. package/lib/session-query.mjs +150 -0
  7. package/lib/startup.mjs +4 -4
  8. package/lib/{theme-DCT8Y2xf.mjs → theme-7u5Qo3dF.mjs} +657 -20
  9. package/lib/types/app.d.ts +106 -62
  10. package/lib/types/authorization-panel.d.ts +3 -3
  11. package/lib/types/git-workflow.d.ts +91 -2
  12. package/lib/types/i18n.d.ts +39 -0
  13. package/lib/types/index.d.ts +100 -1
  14. package/lib/types/input-split.d.ts +1 -1
  15. package/lib/types/kernel-panels.d.ts +107 -29
  16. package/lib/types/language-panel.d.ts +12 -0
  17. package/lib/types/locales/en.d.ts +450 -0
  18. package/lib/types/locales/zh.d.ts +9 -0
  19. package/lib/types/mentions.d.ts +7 -3
  20. package/lib/types/models.d.ts +14 -0
  21. package/lib/types/panel-accent.d.ts +28 -0
  22. package/lib/types/rainbow.d.ts +69 -0
  23. package/lib/types/render/animations.d.ts +42 -0
  24. package/lib/types/render/editor.d.ts +4 -3
  25. package/lib/types/render/ime-cursor.d.ts +60 -0
  26. package/lib/types/render/inspector.d.ts +26 -0
  27. package/lib/types/render/lines.d.ts +21 -1
  28. package/lib/types/render/markdown.d.ts +1 -1
  29. package/lib/types/render/projection.d.ts +130 -4
  30. package/lib/types/render/status.d.ts +9 -9
  31. package/lib/types/render/text.d.ts +6 -0
  32. package/lib/types/render/usage.d.ts +113 -0
  33. package/lib/types/session-directory.d.ts +17 -0
  34. package/lib/types/session-query.d.ts +92 -0
  35. package/lib/types/startup.d.ts +1 -1
  36. package/lib/types/terminal-title.d.ts +66 -0
  37. package/lib/types/theme-panel.d.ts +2 -2
  38. package/lib/types/theme.d.ts +271 -52
  39. package/lib/types/update-panel.d.ts +49 -0
  40. package/lib/types/update.d.ts +75 -0
  41. package/lib/types/version.d.ts +4 -3
  42. package/package.json +246 -90
  43. package/src/app.ts +1369 -509
  44. package/src/approval.ts +166 -166
  45. package/src/authorization-panel.ts +19 -16
  46. package/src/editor-keys.ts +371 -371
  47. package/src/git-workflow.ts +229 -3
  48. package/src/i18n.ts +68 -0
  49. package/src/index.ts +534 -80
  50. package/src/input-split.ts +3 -3
  51. package/src/internals.ts +5 -0
  52. package/src/kernel-panels.ts +554 -86
  53. package/src/keyboard.ts +5 -4
  54. package/src/language-panel.ts +53 -0
  55. package/src/locales/en.ts +489 -0
  56. package/src/locales/zh.ts +488 -0
  57. package/src/mentions.ts +8 -4
  58. package/src/models.ts +264 -212
  59. package/src/panel-accent.ts +41 -0
  60. package/src/presets.ts +1 -1
  61. package/src/provider-settings.ts +1 -1
  62. package/src/rainbow.ts +208 -0
  63. package/src/render/animations.ts +104 -6
  64. package/src/render/editor.ts +25 -24
  65. package/src/render/export.ts +116 -95
  66. package/src/render/ime-cursor.ts +147 -0
  67. package/src/render/inspector.ts +42 -0
  68. package/src/render/lines.ts +628 -415
  69. package/src/render/markdown.ts +15 -3
  70. package/src/render/projection.ts +572 -21
  71. package/src/render/status.ts +59 -39
  72. package/src/render/text.ts +14 -0
  73. package/src/render/tool-preview.ts +77 -77
  74. package/src/render/usage.ts +430 -0
  75. package/src/render/width.ts +2 -2
  76. package/src/session-directory.ts +8 -6
  77. package/src/session-query.ts +239 -0
  78. package/src/startup.ts +3 -3
  79. package/src/subagents.ts +229 -229
  80. package/src/terminal-title.ts +190 -0
  81. package/src/theme-panel.ts +17 -21
  82. package/src/theme.ts +281 -33
  83. package/src/update-panel.ts +256 -0
  84. package/src/update.ts +126 -0
  85. package/src/version.ts +58 -20
  86. package/src/whale-glyph.ts +23 -23
package/src/approval.ts CHANGED
@@ -1,166 +1,166 @@
1
- /**
2
- * The terminal approval answerer: one `approval/request` waterfall listener
3
- * that renders the pending question as a y/n bar and resolves the decision
4
- * back into the waterfall. Mirrors the web host's composer takeover — the
5
- * service (audit pair, policy gate, fail-closed defaults) all live in
6
- * dsh-base; this module only answers for agents this TUI owns.
7
- *
8
- * Vocabulary note: a client answerer may only ever resolve `'allowed-once'`
9
- * or `'rejected'`; `'cancelled'` belongs to the request signal and
10
- * `'unavailable'` to the fail-closed waterfall default.
11
- *
12
- * @module @deepseek-ai/dsh-code/approval
13
- */
14
-
15
- import type { Context } from '@deepseek-ai/cordis'
16
- import type { Agent } from '@deepseek-ai/dsh-agent'
17
- import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
18
-
19
- /** The answer values a client answerer may resolve with. */
20
- export type ApprovalAnswer = 'allowed-once' | 'rejected'
21
-
22
- /** One pending approval question, derived from the request for rendering. */
23
- export interface PendingApproval {
24
- /** The asker's human-readable explanation, or a generic fallback. */
25
- headline: string
26
- /** The tool the question is about. */
27
- toolName: string
28
- /** Command-line preview resolved from the paired streaming tool call. */
29
- command: string
30
- /** Resolve the ask; calling twice is inert (one-shot latch). */
31
- answer(outcome: ApprovalAnswer): void
32
- }
33
-
34
- /** The pending-question snapshot the renderer subscribes to. */
35
- export interface ApprovalSnapshot {
36
- /** The question on screen (queue head), or undefined when none is asked. */
37
- pending: PendingApproval | undefined
38
- /** Presentational: an answer was submitted, the ask has not settled yet. */
39
- answered: boolean
40
- /** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
41
- queued: number
42
- }
43
-
44
- /** Store the pending question lands in; the renderer reads, the answerer writes. */
45
- export interface ApprovalStore {
46
- /** Subscribe to pending-state changes; returns the unsubscribe function. */
47
- subscribe(listener: () => void): () => void
48
- /** Read the current snapshot (identity-stable between changes). */
49
- getSnapshot(): ApprovalSnapshot
50
- }
51
-
52
- /**
53
- * Create the approval store and mount the answerer listener on the context.
54
- * The listener claims only requests for `owns`-owned agents and defers every
55
- * other request back into the waterfall (`next()`), so sibling answerers stay
56
- * usable. An aborted ask never reaches the human. Plugin teardown removes the
57
- * listener; the service then fails its own question closed.
58
- * @param ctx - plugin context whose event bus carries `approval/request`.
59
- * @param owns - agents this terminal answers for.
60
- * @param preview - resolves a tool-call preview for a pending request (the
61
- * request contract carries no arguments; the UI self-serves from the
62
- * transcript projection via `callId`).
63
- * @returns the store the renderer subscribes to.
64
- */
65
- export function mountApprovalAnswerer(
66
- ctx: Context,
67
- owns: (agent: Agent) => boolean,
68
- preview: (request: ApprovalRequest) => string,
69
- ): ApprovalStore {
70
- /** One live ask: its pending view plus the one-shot settle plumbing. */
71
- interface Slot {
72
- readonly pending: PendingApproval
73
- answered: boolean
74
- }
75
- const queue: Slot[] = []
76
- let snapshot: ApprovalSnapshot = { pending: undefined, answered: false, queued: 0 }
77
- const listeners = new Set<() => void>()
78
- const publish = (): void => {
79
- const head = queue[0]
80
- snapshot = {
81
- pending: head === undefined ? undefined : head.pending,
82
- answered: head !== undefined && head.answered,
83
- queued: Math.max(0, queue.length - 1),
84
- }
85
- for (const listener of listeners) listener()
86
- }
87
- const removeSlot = (slot: Slot): void => {
88
- const at = queue.indexOf(slot)
89
- if (at !== -1) queue.splice(at, 1)
90
- }
91
-
92
- ctx.on('approval/request', (request: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => {
93
- if (!owns(request.agent)) return next()
94
- // An already-aborted ask never reaches the human (mirrors the host bridge).
95
- if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled')
96
-
97
- let resolved = false
98
- let settle!: (outcome: ApprovalOutcome) => void
99
- // Established BEFORE the abort listener mounts: a synchronous throw
100
- // between the listener registration and a later construction site would
101
- // otherwise leave a subsequent abort invoking an unassigned settle from
102
- // inside the AbortSignal listener (an uncaughtException).
103
- const settled = new Promise<ApprovalOutcome>((resolve) => {
104
- settle = resolve
105
- })
106
- const signal = request.signal
107
- const onAbort = (): void => withdraw()
108
- // Detach on every settle so an answered ask never retains a listener on
109
- // the tool call's signal (long turns ask many times; each ask must let go).
110
- const detachAbort = (): void => {
111
- if (signal !== undefined) signal.removeEventListener('abort', onAbort)
112
- }
113
- const withdraw = (): void => {
114
- if (resolved) return
115
- resolved = true
116
- detachAbort()
117
- removeSlot(slot)
118
- publish()
119
- // The service's signal race would conclude 'cancelled' anyway; settle
120
- // the same way so this listener never dangles a pending promise.
121
- settle('cancelled')
122
- }
123
- if (signal !== undefined) {
124
- signal.addEventListener('abort', onAbort, { once: true })
125
- }
126
- const slot: Slot = {
127
- answered: false,
128
- pending: {
129
- headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
130
- toolName: request.toolName,
131
- command: preview(request),
132
- answer: (outcome: ApprovalAnswer): void => {
133
- // One-shot latch: a second keypress after submission is inert.
134
- if (resolved) return
135
- resolved = true
136
- detachAbort()
137
- slot.answered = true
138
- publish()
139
- settle(outcome)
140
- },
141
- },
142
- }
143
- queue.push(slot)
144
- publish()
145
-
146
- return settled.then((outcome) => {
147
- if (outcome !== 'cancelled') {
148
- removeSlot(slot)
149
- publish()
150
- }
151
- return outcome
152
- })
153
- })
154
-
155
- return {
156
- subscribe(listener: () => void): () => void {
157
- listeners.add(listener)
158
- return () => {
159
- listeners.delete(listener)
160
- }
161
- },
162
- getSnapshot(): ApprovalSnapshot {
163
- return snapshot
164
- },
165
- }
166
- }
1
+ /**
2
+ * The terminal approval answerer: one `approval/request` waterfall listener
3
+ * that renders the pending question as a y/n bar and resolves the decision
4
+ * back into the waterfall. Mirrors the web host's composer takeover — the
5
+ * service (audit pair, policy gate, fail-closed defaults) all live in
6
+ * dsh-base; this module only answers for agents this TUI owns.
7
+ *
8
+ * Vocabulary note: a client answerer may only ever resolve `'allowed-once'`
9
+ * or `'rejected'`; `'cancelled'` belongs to the request signal and
10
+ * `'unavailable'` to the fail-closed waterfall default.
11
+ *
12
+ * @module @deepseek-ai/dsh-code/approval
13
+ */
14
+
15
+ import type { Context } from '@deepseek-ai/cordis'
16
+ import type { Agent } from '@deepseek-ai/dsh-agent'
17
+ import type { ApprovalOutcome, ApprovalRequest } from '@deepseek-ai/dsh-user-approval'
18
+
19
+ /** The answer values a client answerer may resolve with. */
20
+ export type ApprovalAnswer = 'allowed-once' | 'rejected'
21
+
22
+ /** One pending approval question, derived from the request for rendering. */
23
+ export interface PendingApproval {
24
+ /** The asker's human-readable explanation, or a generic fallback. */
25
+ headline: string
26
+ /** The tool the question is about. */
27
+ toolName: string
28
+ /** Command-line preview resolved from the paired streaming tool call. */
29
+ command: string
30
+ /** Resolve the ask; calling twice is inert (one-shot latch). */
31
+ answer(outcome: ApprovalAnswer): void
32
+ }
33
+
34
+ /** The pending-question snapshot the renderer subscribes to. */
35
+ export interface ApprovalSnapshot {
36
+ /** The question on screen (queue head), or undefined when none is asked. */
37
+ pending: PendingApproval | undefined
38
+ /** Presentational: an answer was submitted, the ask has not settled yet. */
39
+ answered: boolean
40
+ /** Further asks waiting behind the on-screen one (FIFO, Codex-style). */
41
+ queued: number
42
+ }
43
+
44
+ /** Store the pending question lands in; the renderer reads, the answerer writes. */
45
+ export interface ApprovalStore {
46
+ /** Subscribe to pending-state changes; returns the unsubscribe function. */
47
+ subscribe(listener: () => void): () => void
48
+ /** Read the current snapshot (identity-stable between changes). */
49
+ getSnapshot(): ApprovalSnapshot
50
+ }
51
+
52
+ /**
53
+ * Create the approval store and mount the answerer listener on the context.
54
+ * The listener claims only requests for `owns`-owned agents and defers every
55
+ * other request back into the waterfall (`next()`), so sibling answerers stay
56
+ * usable. An aborted ask never reaches the human. Plugin teardown removes the
57
+ * listener; the service then fails its own question closed.
58
+ * @param ctx - plugin context whose event bus carries `approval/request`.
59
+ * @param owns - agents this terminal answers for.
60
+ * @param preview - resolves a tool-call preview for a pending request (the
61
+ * request contract carries no arguments; the UI self-serves from the
62
+ * transcript projection via `callId`).
63
+ * @returns the store the renderer subscribes to.
64
+ */
65
+ export function mountApprovalAnswerer(
66
+ ctx: Context,
67
+ owns: (agent: Agent) => boolean,
68
+ preview: (request: ApprovalRequest) => string,
69
+ ): ApprovalStore {
70
+ /** One live ask: its pending view plus the one-shot settle plumbing. */
71
+ interface Slot {
72
+ readonly pending: PendingApproval
73
+ answered: boolean
74
+ }
75
+ const queue: Slot[] = []
76
+ let snapshot: ApprovalSnapshot = { pending: undefined, answered: false, queued: 0 }
77
+ const listeners = new Set<() => void>()
78
+ const publish = (): void => {
79
+ const head = queue[0]
80
+ snapshot = {
81
+ pending: head === undefined ? undefined : head.pending,
82
+ answered: head !== undefined && head.answered,
83
+ queued: Math.max(0, queue.length - 1),
84
+ }
85
+ for (const listener of listeners) listener()
86
+ }
87
+ const removeSlot = (slot: Slot): void => {
88
+ const at = queue.indexOf(slot)
89
+ if (at !== -1) queue.splice(at, 1)
90
+ }
91
+
92
+ ctx.on('approval/request', (request: ApprovalRequest, next: () => Promise<ApprovalOutcome>) => {
93
+ if (!owns(request.agent)) return next()
94
+ // An already-aborted ask never reaches the human (mirrors the host bridge).
95
+ if (request.signal?.aborted === true) return Promise.resolve<ApprovalOutcome>('cancelled')
96
+
97
+ let resolved = false
98
+ let settle!: (outcome: ApprovalOutcome) => void
99
+ // Established BEFORE the abort listener mounts: a synchronous throw
100
+ // between the listener registration and a later construction site would
101
+ // otherwise leave a subsequent abort invoking an unassigned settle from
102
+ // inside the AbortSignal listener (an uncaughtException).
103
+ const settled = new Promise<ApprovalOutcome>((resolve) => {
104
+ settle = resolve
105
+ })
106
+ const signal = request.signal
107
+ const onAbort = (): void => withdraw()
108
+ // Detach on every settle so an answered ask never retains a listener on
109
+ // the tool call's signal (long turns ask many times; each ask must let go).
110
+ const detachAbort = (): void => {
111
+ if (signal !== undefined) signal.removeEventListener('abort', onAbort)
112
+ }
113
+ const withdraw = (): void => {
114
+ if (resolved) return
115
+ resolved = true
116
+ detachAbort()
117
+ removeSlot(slot)
118
+ publish()
119
+ // The service's signal race would conclude 'cancelled' anyway; settle
120
+ // the same way so this listener never dangles a pending promise.
121
+ settle('cancelled')
122
+ }
123
+ if (signal !== undefined) {
124
+ signal.addEventListener('abort', onAbort, { once: true })
125
+ }
126
+ const slot: Slot = {
127
+ answered: false,
128
+ pending: {
129
+ headline: request.reason ?? `tool ${request.toolName} asks for your approval`,
130
+ toolName: request.toolName,
131
+ command: preview(request),
132
+ answer: (outcome: ApprovalAnswer): void => {
133
+ // One-shot latch: a second keypress after submission is inert.
134
+ if (resolved) return
135
+ resolved = true
136
+ detachAbort()
137
+ slot.answered = true
138
+ publish()
139
+ settle(outcome)
140
+ },
141
+ },
142
+ }
143
+ queue.push(slot)
144
+ publish()
145
+
146
+ return settled.then((outcome) => {
147
+ if (outcome !== 'cancelled') {
148
+ removeSlot(slot)
149
+ publish()
150
+ }
151
+ return outcome
152
+ })
153
+ })
154
+
155
+ return {
156
+ subscribe(listener: () => void): () => void {
157
+ listeners.add(listener)
158
+ return () => {
159
+ listeners.delete(listener)
160
+ }
161
+ },
162
+ getSnapshot(): ApprovalSnapshot {
163
+ return snapshot
164
+ },
165
+ }
166
+ }
@@ -13,7 +13,9 @@ import type { CredentialKey } from '@deepseek-ai/dsh-credentials'
13
13
  import type { ProviderAuthorizationRow } from './authorization.ts'
14
14
  import { panelViewport } from './render/inspector.ts'
15
15
  import { displayText, singleLineText, truncateColumns } from './render/text.ts'
16
+ import { panelAccent } from './panel-accent.ts'
16
17
  import { getPalette, inkColor } from './theme.ts'
18
+ import { t } from './i18n.ts'
17
19
 
18
20
  interface PromptReply {
19
21
  readonly resolve: (value: string) => void
@@ -154,7 +156,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
154
156
  setCursor(current => (current + 1) % props.row.methods.length)
155
157
  return
156
158
  }
157
- if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0]!.id)
159
+ if (key.return) start(props.row.methods[cursor]?.id ?? props.row.methods[0].id)
158
160
  return
159
161
  }
160
162
 
@@ -182,7 +184,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
182
184
  setPromptCursor(current => (current + 1) % prompt.options.length)
183
185
  return
184
186
  }
185
- if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0]!.id)
187
+ if (key.return) answer(prompt.options[promptCursor]?.id ?? prompt.options[0].id)
186
188
  return
187
189
  }
188
190
  if (key.backspace || key.delete) {
@@ -197,7 +199,7 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
197
199
  })
198
200
 
199
201
  if (viewport.maxHeight === 0 || viewport.compact) {
200
- return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('provider login · esc cancel', viewport.contentColumns))
202
+ return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.compact'), viewport.contentColumns))
201
203
  }
202
204
 
203
205
  const rows: Array<{ key: string; text: string; color?: string; bold?: boolean }> = []
@@ -229,18 +231,19 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
229
231
  rows.push({ key: 'draft', text: ` ${shown}▏`, color: inkColor(getPalette().text) })
230
232
  }
231
233
  } else {
232
- rows.push({ key: 'waiting', text: ' waiting for provider…', color: inkColor(getPalette().dim) })
234
+ rows.push({ key: 'waiting', text: ` ${t('panel.auth.waiting')}`, color: inkColor(getPalette().dim) })
233
235
  }
234
236
  if (copyState !== undefined) rows.push({ key: 'copy', text: ` ${singleLineText(copyState)}`, color: inkColor(copyState === 'copied' ? getPalette().success : getPalette().error) })
235
237
  }
236
238
  const visible = rows.slice(Math.max(0, rows.length - viewport.bodyRows))
237
239
  const footer = phase === 'methods'
238
- ? '↑↓ choose · enter continue · esc/q back'
239
- : 'enter answer · c copy URL/code · esc cancel login'
240
+ ? t('panel.auth.methodsFooter')
241
+ : t('panel.auth.answerFooter')
242
+ const accent = panelAccent('auth-login', getPalette().brand)
240
243
  return createElement(
241
244
  Box,
242
- { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().brand) },
243
- createElement(Text, { color: inkColor(getPalette().brand), bold: true, wrap: 'truncate-end' }, truncateColumns(`/model · login ${displayText(props.row.label)}`, viewport.contentColumns)),
245
+ { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(accent.border) },
246
+ createElement(Text, { color: inkColor(accent.title), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.title', { provider: displayText(props.row.label) }), viewport.contentColumns)),
244
247
  ...visible.map(row => createElement(Text, { key: row.key, color: row.color, bold: row.bold, wrap: 'truncate-end' }, truncateColumns(row.text, viewport.contentColumns))),
245
248
  createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(footer, viewport.contentColumns)),
246
249
  )
@@ -248,9 +251,9 @@ export function ProviderAuthorizationPanel(props: ProviderAuthorizationPanelProp
248
251
 
249
252
  export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }: {
250
253
  row: ProviderAuthorizationRow
251
- confirm(row: ProviderAuthorizationRow): Promise<void>
252
- done(): void
253
- back(): void
254
+ confirm: (row: ProviderAuthorizationRow) => Promise<void>
255
+ done: () => void
256
+ back: () => void
254
257
  }): ReactElement {
255
258
  const stdout = useStdout().stdout
256
259
  const viewport = panelViewport(stdout?.columns ?? 80, stdout?.rows ?? 30)
@@ -270,14 +273,14 @@ export function ProviderAuthorizationLogoutPanel({ row, confirm, done, back }: {
270
273
  setError(reason instanceof Error ? reason.message : String(reason))
271
274
  })
272
275
  })
273
- if (viewport.maxHeight === 0 || viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns('y logout · n/esc back', viewport.contentColumns))
276
+ if (viewport.maxHeight === 0 || viewport.compact) return createElement(Text, { wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutCompact'), viewport.contentColumns))
274
277
  return createElement(
275
278
  Box,
276
279
  { flexDirection: 'column', width: viewport.outerColumns, paddingX: 1, borderStyle: 'round', borderColor: inkColor(getPalette().warn) },
277
- createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns('/model · logout provider', viewport.contentColumns)),
278
- createElement(Text, { wrap: 'truncate-end' }, truncateColumns(` remove ${displayText(row.label)} login record`, viewport.contentColumns)),
279
- createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(' provider endpoint and model configuration stay unchanged', viewport.contentColumns)),
280
+ createElement(Text, { color: inkColor(getPalette().warn), bold: true, wrap: 'truncate-end' }, truncateColumns(t('panel.auth.logoutTitle'), viewport.contentColumns)),
281
+ createElement(Text, { wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.removeRecord', { provider: displayText(row.label) })}`, viewport.contentColumns)),
282
+ createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(` ${t('panel.auth.unchanged')}`, viewport.contentColumns)),
280
283
  error === undefined ? undefined : createElement(Text, { color: inkColor(getPalette().error), wrap: 'truncate-end' }, truncateColumns(` ${singleLineText(error)}`, viewport.contentColumns)),
281
- createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(busy ? 'working' : 'y confirm · n/esc back', viewport.contentColumns)),
284
+ createElement(Text, { color: inkColor(getPalette().dim), wrap: 'truncate-end' }, truncateColumns(busy ? t('panel.auth.working') : t('panel.auth.logoutFooter'), viewport.contentColumns)),
282
285
  )
283
286
  }