@vibe-forge/client 0.11.0 → 0.11.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/dist/assets/{arc-M4HYfcHs.js → arc-CSepokz3.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-CUrDjrxj.js → blockDiagram-c4efeb88-D0ARcoNf.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-BMEtqlFp.js → c4Diagram-c83219d4-BysYF9kP.js} +1 -1
- package/dist/assets/channel-CeKPk6Nd.js +1 -0
- package/dist/assets/{classDiagram-beda092f-BOmDJ0Ml.js → classDiagram-beda092f-BG1GhIOL.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-BODzX2MB.js → classDiagram-v2-2358418a-Dd08uGSH.js} +1 -1
- package/dist/assets/clone-CrkD2PuD.js +1 -0
- package/dist/assets/{createText-1719965b-B9Dd8zcR.js → createText-1719965b-CigPEIEn.js} +1 -1
- package/dist/assets/{cssMode-DLxG92Ot.js → cssMode-MjflyEfm.js} +1 -1
- package/dist/assets/{edges-96097737-CuZFd43m.js → edges-96097737-DuTBJJRv.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-8g9lu2-Z.js → erDiagram-0228fc6a-Cp1bL7Y7.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-BlBS1tdN.js → flowDb-c6c81e3f-BfKbhiq5.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-u6mWflpF.js → flowDiagram-50d868cf-m7gGc3PK.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-4ZU4bdp1.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-BDqI2NFr.js → flowchart-elk-definition-6af322e1-EVeTDRRK.js} +1 -1
- package/dist/assets/{freemarker2-tVtpTMPu.js → freemarker2-Bb3-QAIN.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-CDQjx9Wu.js → ganttDiagram-a2739b55-DslB2U0R.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-DUHFKRVA.js → gitGraphDiagram-82fe8481-C-KFWMXL.js} +1 -1
- package/dist/assets/{graph-2HKPi5B_.js → graph-CukaUc0o.js} +1 -1
- package/dist/assets/{handlebars-D00tgNd8.js → handlebars-C4le-2Y6.js} +1 -1
- package/dist/assets/{html-B-TDzBiR.js → html-CjNiRs5S.js} +1 -1
- package/dist/assets/{htmlMode-ClycqSTM.js → htmlMode-B73_3-We.js} +1 -1
- package/dist/assets/{index-5325376f-DPrJpRQ-.js → index-5325376f-CVISZFPw.js} +1 -1
- package/dist/assets/{index-CAHZZEoo.js → index-BZosmb5_.js} +330 -326
- package/dist/assets/index-C1oh0w9H.css +32 -0
- package/dist/assets/{infoDiagram-8eee0895-Co5tS1I5.js → infoDiagram-8eee0895-DoirLE1K.js} +1 -1
- package/dist/assets/{javascript-zbkwarmb.js → javascript-BDjnqJFP.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-k_qioHgy.js → journeyDiagram-c64418c1-Ckn-p2CM.js} +1 -1
- package/dist/assets/{jsonMode-C3CSpzBF.js → jsonMode-C-ftOc5j.js} +1 -1
- package/dist/assets/{layout-CjOXKxvs.js → layout-Z7yUG7hB.js} +1 -1
- package/dist/assets/{line-C-XnQrKR.js → line-DPG_cfAy.js} +1 -1
- package/dist/assets/{linear-C7MMERzS.js → linear--GSeVfMi.js} +1 -1
- package/dist/assets/{liquid-5G37EU6K.js → liquid-COiLZ9py.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-zaDMuhCE.js → lspLanguageFeatures-DGmhryFq.js} +1 -1
- package/dist/assets/{mdx-Bc-LY0gi.js → mdx-BpL87Gej.js} +1 -1
- package/dist/assets/{mermaid.core-CechbHof.js → mermaid.core-Cg1CCDo6.js} +4 -4
- package/dist/assets/{mindmap-definition-8da855dc-ejftCDGb.js → mindmap-definition-8da855dc-CKDof1lD.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-DY__X3Qj.js → pieDiagram-a8764435-DwvCaZVE.js} +1 -1
- package/dist/assets/{python-vK2Ff2J5.js → python-63dBmWV_.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-azIZCv_2.js → quadrantDiagram-1e28029f-CkzYBQpy.js} +1 -1
- package/dist/assets/{razor-BipjBJKu.js → razor-C50tBqEZ.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-C4EB0Xs2.js → requirementDiagram-08caed73-Brgdjqf4.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-PNhR6YWu.js → sankeyDiagram-a04cb91d-CGkYexrs.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-4c-qV-Ri.js → sequenceDiagram-c5b8d532-D0wE-_J8.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-CnURumPE.js → stateDiagram-1ecb1508-BYb3NCXZ.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-DR2qHTPg.js → stateDiagram-v2-c2b004d7-DrPqi4Pt.js} +1 -1
- package/dist/assets/{styles-b4e223ce-B2PWXT_i.js → styles-b4e223ce-DD66TIO4.js} +1 -1
- package/dist/assets/{styles-ca3715f6-DEhgVF5H.js → styles-ca3715f6-iy02LHIV.js} +1 -1
- package/dist/assets/{styles-d45a18b0-DyzccA5F.js → styles-d45a18b0-BgqAgJyW.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-C_1tMhxp.js → svgDrawCommon-b86b1483-CDq7ugnw.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080-FdaC0dQH.js → timeline-definition-faaaa080-DzcLLjK0.js} +1 -1
- package/dist/assets/{tsMode-CrMC5T3_.js → tsMode-BFRFI4ct.js} +1 -1
- package/dist/assets/{typescript-CRfPu8v7.js → typescript-CBZQRAPv.js} +1 -1
- package/dist/assets/{xml-jlRvQfFI.js → xml-BpWm6upt.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-sxjv75h9.js → xychartDiagram-f5964ef8-zBN8FmLQ.js} +1 -1
- package/dist/assets/{yaml-B47_IHOH.js → yaml-CqbJPiIP.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +6 -6
- package/src/api/git.ts +78 -0
- package/src/api.ts +24 -0
- package/src/components/chat/ChatHeader.tsx +4 -0
- package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +157 -0
- package/src/components/chat/git-controls/ChatGitControls.scss +616 -0
- package/src/components/chat/git-controls/ChatGitControls.tsx +151 -0
- package/src/components/chat/git-controls/GitCommitModal.tsx +199 -0
- package/src/components/chat/git-controls/GitCommitModalParts.tsx +151 -0
- package/src/components/chat/git-controls/GitOperationsDropdown.tsx +123 -0
- package/src/components/chat/git-controls/GitPushModal.tsx +106 -0
- package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +68 -0
- package/src/components/chat/git-controls/git-branch-utils.ts +88 -0
- package/src/components/chat/git-controls/git-commit-utils.ts +79 -0
- package/src/components/chat/git-controls/git-mutation-utils.ts +69 -0
- package/src/components/chat/git-controls/git-operation-utils.ts +98 -0
- package/src/components/chat/git-controls/git-worktree-utils.ts +49 -0
- package/src/components/chat/git-controls/use-chat-git-commit.ts +185 -0
- package/src/components/chat/git-controls/use-chat-git-controls.ts +200 -0
- package/src/components/chat/git-controls/use-chat-git-push-state.ts +19 -0
- package/src/components/chat/git-controls/use-chat-git-worktrees.ts +39 -0
- package/src/components/chat/tools/DefaultTool.tsx +40 -31
- package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +1 -15
- package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +8 -1
- package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +10 -95
- package/src/components/chat/tools/core/ToolCallBox.scss +18 -0
- package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
- package/src/components/chat/tools/core/tool-display.ts +12 -1
- package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
- package/src/components/chat/tools/core/tool-summary.ts +18 -6
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +0 -7
- package/src/hooks/chat/session-view-cache.ts +80 -0
- package/src/hooks/chat/use-chat-session-messages.ts +124 -30
- package/src/resources/locales/en.json +68 -0
- package/src/resources/locales/zh.json +68 -0
- package/dist/assets/channel-Cj3Cp2OJ.js +0 -1
- package/dist/assets/clone-B7Q9B1dS.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-G3v545eF.js +0 -1
- package/dist/assets/index-Di7lePfb.css +0 -32
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { Tooltip } from 'antd'
|
|
2
|
+
import React from 'react'
|
|
3
|
+
|
|
4
|
+
import { CodeBlock } from '#~/components/CodeBlock'
|
|
5
|
+
import { safeJsonStringify } from '#~/utils/safe-serialize'
|
|
6
|
+
|
|
7
|
+
import { TOOL_TOOLTIP_PROPS, getToolFieldIcon, getToolInlineValueText, getToolValueText } from './tool-display'
|
|
8
|
+
|
|
9
|
+
type Translate = (key: string, options?: Record<string, unknown>) => string
|
|
10
|
+
|
|
11
|
+
export type ToolFieldFormat = 'inline' | 'text' | 'code' | 'list' | 'json' | 'questions'
|
|
12
|
+
|
|
13
|
+
export interface ToolFieldView {
|
|
14
|
+
labelKey: string
|
|
15
|
+
fallbackLabel: string
|
|
16
|
+
format: ToolFieldFormat
|
|
17
|
+
value: unknown
|
|
18
|
+
lang?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const getFieldKey = (field: ToolFieldView, index: number) => `${field.labelKey}-${index}`
|
|
22
|
+
|
|
23
|
+
const getSectionHeader = (icon: string, label: string) => (
|
|
24
|
+
<div className='tool-detail-section__header'>
|
|
25
|
+
<Tooltip title={label} {...TOOL_TOOLTIP_PROPS}>
|
|
26
|
+
<span className='tool-detail-section__icon material-symbols-rounded'>{icon}</span>
|
|
27
|
+
</Tooltip>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
export function ToolInlineFields({
|
|
32
|
+
fields,
|
|
33
|
+
t
|
|
34
|
+
}: {
|
|
35
|
+
fields: ToolFieldView[]
|
|
36
|
+
t: Translate
|
|
37
|
+
}) {
|
|
38
|
+
if (fields.length === 0) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className='tool-inline-token-list tool-inline-token-list--standalone'
|
|
45
|
+
aria-label={t('chat.tools.fields.details')}
|
|
46
|
+
>
|
|
47
|
+
{fields.map((field, index) => {
|
|
48
|
+
const label = t(field.labelKey, { defaultValue: field.fallbackLabel })
|
|
49
|
+
const valueText = getToolInlineValueText(field.value)
|
|
50
|
+
return (
|
|
51
|
+
<Tooltip
|
|
52
|
+
key={getFieldKey(field, index)}
|
|
53
|
+
title={
|
|
54
|
+
<div className='tool-tooltip-content'>
|
|
55
|
+
<div className='tool-tooltip-content__title'>{label}</div>
|
|
56
|
+
<div className='tool-tooltip-content__value'>{getToolValueText(field.value)}</div>
|
|
57
|
+
</div>
|
|
58
|
+
}
|
|
59
|
+
{...TOOL_TOOLTIP_PROPS}
|
|
60
|
+
>
|
|
61
|
+
<div className='tool-inline-token'>
|
|
62
|
+
<span className='tool-inline-token__icon material-symbols-rounded'>
|
|
63
|
+
{getToolFieldIcon(field.labelKey, field.format)}
|
|
64
|
+
</span>
|
|
65
|
+
<span className='tool-inline-token__value'>{valueText}</span>
|
|
66
|
+
</div>
|
|
67
|
+
</Tooltip>
|
|
68
|
+
)
|
|
69
|
+
})}
|
|
70
|
+
</div>
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderToolBlockField(
|
|
75
|
+
field: ToolFieldView,
|
|
76
|
+
index: number,
|
|
77
|
+
t: Translate,
|
|
78
|
+
options: {
|
|
79
|
+
sectionClassName?: string
|
|
80
|
+
} = {}
|
|
81
|
+
) {
|
|
82
|
+
const label = t(field.labelKey, { defaultValue: field.fallbackLabel })
|
|
83
|
+
const sectionHeader = getSectionHeader(getToolFieldIcon(field.labelKey, field.format), label)
|
|
84
|
+
const sectionClassName = options.sectionClassName ?? 'tool-detail-section'
|
|
85
|
+
|
|
86
|
+
if (field.format === 'text') {
|
|
87
|
+
return (
|
|
88
|
+
<div className={sectionClassName} key={getFieldKey(field, index)}>
|
|
89
|
+
{sectionHeader}
|
|
90
|
+
<div className='tool-detail-section__text'>{String(field.value)}</div>
|
|
91
|
+
</div>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (field.format === 'code') {
|
|
96
|
+
return (
|
|
97
|
+
<div className={sectionClassName} key={getFieldKey(field, index)}>
|
|
98
|
+
{sectionHeader}
|
|
99
|
+
<CodeBlock
|
|
100
|
+
code={String(field.value)}
|
|
101
|
+
lang={field.lang ?? 'text'}
|
|
102
|
+
hideHeader={true}
|
|
103
|
+
/>
|
|
104
|
+
</div>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (field.format === 'list') {
|
|
109
|
+
const items = Array.isArray(field.value) ? field.value.map(item => String(item)) : []
|
|
110
|
+
return (
|
|
111
|
+
<div className={sectionClassName} key={getFieldKey(field, index)}>
|
|
112
|
+
{sectionHeader}
|
|
113
|
+
<div className='tool-detail-list'>
|
|
114
|
+
{items.map(listItem => (
|
|
115
|
+
<div className='tool-detail-list-item' key={listItem}>{listItem}</div>
|
|
116
|
+
))}
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<div className={sectionClassName} key={getFieldKey(field, index)}>
|
|
124
|
+
{sectionHeader}
|
|
125
|
+
<CodeBlock
|
|
126
|
+
code={safeJsonStringify(field.value, 2)}
|
|
127
|
+
lang='json'
|
|
128
|
+
hideHeader={true}
|
|
129
|
+
/>
|
|
130
|
+
</div>
|
|
131
|
+
)
|
|
132
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
|
|
1
3
|
import type { ChatMessageContent } from '@vibe-forge/core'
|
|
2
4
|
|
|
3
5
|
import {
|
|
@@ -6,6 +8,7 @@ import {
|
|
|
6
8
|
isClaudeToolName
|
|
7
9
|
} from '../adapter-claude/claude-tool-presentation'
|
|
8
10
|
import { getClaudeToolSummaryText } from '../adapter-claude/claude-tool-summary'
|
|
11
|
+
import { buildGenericToolPresentation } from './generic-tool-presentation'
|
|
9
12
|
|
|
10
13
|
export type ToolUseItem = Extract<ChatMessageContent, { type: 'tool_use' }>
|
|
11
14
|
type Translate = (key: string, options?: Record<string, unknown>) => string
|
|
@@ -28,7 +31,7 @@ export const formatToolName = (name: string) => {
|
|
|
28
31
|
return name.replace('mcp__ChromeDevtools__', '')
|
|
29
32
|
}
|
|
30
33
|
|
|
31
|
-
const namespaceSegments = name.split('__').filter(Boolean)
|
|
34
|
+
const namespaceSegments = name.includes('__') ? name.split('__').filter(Boolean) : []
|
|
32
35
|
const lastSegment = namespaceSegments.length > 0
|
|
33
36
|
? namespaceSegments[namespaceSegments.length - 1]
|
|
34
37
|
: name.split(':').pop() ?? name
|
|
@@ -76,8 +79,11 @@ export function getToolSummaryText(item: ToolUseItem, t: Translate) {
|
|
|
76
79
|
return getClaudeToolSummaryText(item.name, item.input, t)
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
const
|
|
80
|
-
const
|
|
82
|
+
const presentation = buildGenericToolPresentation(item.name, item.input)
|
|
83
|
+
const displayName = presentation.titleKey != null
|
|
84
|
+
? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
85
|
+
: presentation.fallbackTitle
|
|
86
|
+
const preview = presentation.primary ?? getToolInputPreview(item.input)
|
|
81
87
|
return preview != null && preview !== '' ? `${displayName} ${preview}` : displayName
|
|
82
88
|
}
|
|
83
89
|
|
|
@@ -87,7 +93,10 @@ export function getToolTitleText(item: ToolUseItem, t: Translate) {
|
|
|
87
93
|
return t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
|
|
96
|
+
const presentation = buildGenericToolPresentation(item.name, item.input)
|
|
97
|
+
return presentation.titleKey != null
|
|
98
|
+
? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
99
|
+
: presentation.fallbackTitle
|
|
91
100
|
}
|
|
92
101
|
|
|
93
102
|
export function getToolPrimaryText(item: ToolUseItem) {
|
|
@@ -95,7 +104,7 @@ export function getToolPrimaryText(item: ToolUseItem) {
|
|
|
95
104
|
return buildClaudeToolPresentation(item.name, item.input).primary
|
|
96
105
|
}
|
|
97
106
|
|
|
98
|
-
return getToolInputPreview(item.input)
|
|
107
|
+
return buildGenericToolPresentation(item.name, item.input).primary ?? getToolInputPreview(item.input)
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
const getToolNamespaceLabel = (name: string) => {
|
|
@@ -133,7 +142,10 @@ function getToolGroupDescriptor(item: ToolUseItem, t: Translate): ToolGroupDescr
|
|
|
133
142
|
}
|
|
134
143
|
}
|
|
135
144
|
|
|
136
|
-
const
|
|
145
|
+
const presentation = buildGenericToolPresentation(item.name, item.input)
|
|
146
|
+
const label = presentation.titleKey != null
|
|
147
|
+
? t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
148
|
+
: presentation.fallbackTitle
|
|
137
149
|
return {
|
|
138
150
|
key: item.name,
|
|
139
151
|
label,
|
|
@@ -69,13 +69,6 @@ export const ChromeDevtoolsTool = defineToolRender(({ item, resultItem }) => {
|
|
|
69
69
|
)}
|
|
70
70
|
{hasResultDetails && resultItem != null && (
|
|
71
71
|
<div className='tool-detail-section'>
|
|
72
|
-
<div className='tool-detail-section__header'>
|
|
73
|
-
<Tooltip title={t('chat.result')} {...TOOL_TOOLTIP_PROPS}>
|
|
74
|
-
<span className='tool-detail-section__icon material-symbols-rounded'>
|
|
75
|
-
{getToolSectionIcon('result')}
|
|
76
|
-
</span>
|
|
77
|
-
</Tooltip>
|
|
78
|
-
</div>
|
|
79
72
|
<ToolResultContent content={resultItem.content} />
|
|
80
73
|
</div>
|
|
81
74
|
)}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ChatMessage } from '@vibe-forge/core'
|
|
2
|
+
import type { SessionInfo } from '@vibe-forge/types'
|
|
3
|
+
|
|
4
|
+
import type { ChatErrorState, InteractionRequestState } from './interaction-state'
|
|
5
|
+
|
|
6
|
+
export interface ChatSessionViewSnapshot {
|
|
7
|
+
messages: ChatMessage[]
|
|
8
|
+
sessionInfo: SessionInfo | null
|
|
9
|
+
errorState: ChatErrorState | null
|
|
10
|
+
interactionRequest: InteractionRequestState | null
|
|
11
|
+
isHydrated: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const MAX_CHAT_SESSION_VIEW_SNAPSHOTS = 20
|
|
15
|
+
|
|
16
|
+
export const createChatSessionViewSnapshot = (
|
|
17
|
+
value?: Partial<ChatSessionViewSnapshot>
|
|
18
|
+
): ChatSessionViewSnapshot => ({
|
|
19
|
+
messages: value?.messages ?? [],
|
|
20
|
+
sessionInfo: value?.sessionInfo ?? null,
|
|
21
|
+
errorState: value?.errorState ?? null,
|
|
22
|
+
interactionRequest: value?.interactionRequest ?? null,
|
|
23
|
+
isHydrated: value?.isHydrated ?? false
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export const mergeChatSessionViewSnapshot = (
|
|
27
|
+
current: ChatSessionViewSnapshot | undefined,
|
|
28
|
+
patch: Partial<ChatSessionViewSnapshot>
|
|
29
|
+
): ChatSessionViewSnapshot => {
|
|
30
|
+
return createChatSessionViewSnapshot({
|
|
31
|
+
...createChatSessionViewSnapshot(current),
|
|
32
|
+
...patch
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const restoreChatSessionViewSnapshot = (snapshot?: ChatSessionViewSnapshot) => {
|
|
37
|
+
const resolved = createChatSessionViewSnapshot(snapshot)
|
|
38
|
+
const restorable = resolved.isHydrated === true
|
|
39
|
+
? resolved
|
|
40
|
+
: createChatSessionViewSnapshot()
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
messages: restorable.messages,
|
|
44
|
+
sessionInfo: restorable.sessionInfo,
|
|
45
|
+
errorState: restorable.errorState,
|
|
46
|
+
interactionRequest: restorable.interactionRequest,
|
|
47
|
+
isReady: restorable.isHydrated
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const setChatSessionViewSnapshot = (
|
|
52
|
+
cache: Map<string, ChatSessionViewSnapshot>,
|
|
53
|
+
sessionId: string,
|
|
54
|
+
patch: Partial<ChatSessionViewSnapshot>
|
|
55
|
+
) => {
|
|
56
|
+
const next = mergeChatSessionViewSnapshot(cache.get(sessionId), patch)
|
|
57
|
+
|
|
58
|
+
if (cache.has(sessionId)) {
|
|
59
|
+
cache.delete(sessionId)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
cache.set(sessionId, next)
|
|
63
|
+
|
|
64
|
+
while (cache.size > MAX_CHAT_SESSION_VIEW_SNAPSHOTS) {
|
|
65
|
+
const oldestSessionId = cache.keys().next().value
|
|
66
|
+
if (oldestSessionId == null) {
|
|
67
|
+
break
|
|
68
|
+
}
|
|
69
|
+
cache.delete(oldestSessionId)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return next
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const deleteChatSessionViewSnapshot = (
|
|
76
|
+
cache: Map<string, ChatSessionViewSnapshot>,
|
|
77
|
+
sessionId: string
|
|
78
|
+
) => {
|
|
79
|
+
cache.delete(sessionId)
|
|
80
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
import type { SetStateAction } from 'react'
|
|
2
3
|
import { useTranslation } from 'react-i18next'
|
|
3
4
|
import { useSWRConfig } from 'swr'
|
|
4
5
|
|
|
@@ -8,13 +9,19 @@ import type { SessionInfo } from '@vibe-forge/types'
|
|
|
8
9
|
import { getSessionMessages } from '#~/api.js'
|
|
9
10
|
import { connectionManager } from '#~/connectionManager.js'
|
|
10
11
|
|
|
11
|
-
import type { ChatErrorState } from './interaction-state'
|
|
12
|
+
import type { ChatErrorState, InteractionRequestState } from './interaction-state'
|
|
12
13
|
import {
|
|
13
14
|
applyInteractionStateEvent,
|
|
14
15
|
findLatestFatalError,
|
|
15
16
|
getFatalSessionError,
|
|
16
17
|
restoreInteractionStateFromHistory
|
|
17
18
|
} from './interaction-state'
|
|
19
|
+
import {
|
|
20
|
+
deleteChatSessionViewSnapshot,
|
|
21
|
+
restoreChatSessionViewSnapshot,
|
|
22
|
+
setChatSessionViewSnapshot
|
|
23
|
+
} from './session-view-cache'
|
|
24
|
+
import type { ChatSessionViewSnapshot } from './session-view-cache'
|
|
18
25
|
import type { ChatEffort } from './use-chat-effort'
|
|
19
26
|
import type { PermissionMode } from './use-chat-permission-mode'
|
|
20
27
|
|
|
@@ -62,7 +69,7 @@ export function useChatSessionMessages({
|
|
|
62
69
|
}) {
|
|
63
70
|
const { t } = useTranslation()
|
|
64
71
|
const { mutate } = useSWRConfig()
|
|
65
|
-
const [
|
|
72
|
+
const [messagesState, setMessagesState] = useState<ChatMessage[]>([])
|
|
66
73
|
const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null)
|
|
67
74
|
const [isReady, setIsReady] = useState(false)
|
|
68
75
|
const [errorState, setErrorState] = useState<ChatErrorState | null>(null)
|
|
@@ -74,13 +81,50 @@ export function useChatSessionMessages({
|
|
|
74
81
|
const lastConnectedAdapterRef = useRef<string | undefined>(undefined)
|
|
75
82
|
const lastObservedSessionStatusRef = useRef<Session['status'] | undefined>(session?.status)
|
|
76
83
|
const expectedCloseRef = useRef(false)
|
|
77
|
-
const interactionRequestRef = useRef<
|
|
84
|
+
const interactionRequestRef = useRef<InteractionRequestState | null>(null)
|
|
78
85
|
const activeSessionIdRef = useRef<string | undefined>(session?.id)
|
|
79
86
|
const historyRequestSeqRef = useRef(0)
|
|
80
87
|
const reconcileTimersRef = useRef<Array<ReturnType<typeof setTimeout>>>([])
|
|
88
|
+
const sessionViewCacheRef = useRef(new Map<string, ChatSessionViewSnapshot>())
|
|
81
89
|
|
|
82
90
|
activeSessionIdRef.current = session?.id
|
|
83
91
|
|
|
92
|
+
const updateSessionViewCache = useCallback((
|
|
93
|
+
sessionId: string,
|
|
94
|
+
patch: Partial<{
|
|
95
|
+
messages: ChatMessage[]
|
|
96
|
+
sessionInfo: SessionInfo | null
|
|
97
|
+
errorState: ChatErrorState | null
|
|
98
|
+
interactionRequest: InteractionRequestState | null
|
|
99
|
+
isHydrated: boolean
|
|
100
|
+
}>
|
|
101
|
+
) => {
|
|
102
|
+
return setChatSessionViewSnapshot(sessionViewCacheRef.current, sessionId, patch)
|
|
103
|
+
}, [])
|
|
104
|
+
|
|
105
|
+
const removeSessionViewCache = useCallback((sessionId: string) => {
|
|
106
|
+
deleteChatSessionViewSnapshot(sessionViewCacheRef.current, sessionId)
|
|
107
|
+
}, [])
|
|
108
|
+
|
|
109
|
+
const setMessages = useCallback((value: SetStateAction<ChatMessage[]>) => {
|
|
110
|
+
setMessagesState((current) => {
|
|
111
|
+
const next = typeof value === 'function'
|
|
112
|
+
? value(current)
|
|
113
|
+
: value
|
|
114
|
+
const sessionId = activeSessionIdRef.current
|
|
115
|
+
|
|
116
|
+
if (sessionId != null && sessionId !== '') {
|
|
117
|
+
const currentSnapshot = sessionViewCacheRef.current.get(sessionId)
|
|
118
|
+
updateSessionViewCache(sessionId, {
|
|
119
|
+
messages: next,
|
|
120
|
+
isHydrated: currentSnapshot?.isHydrated === true
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return next
|
|
125
|
+
})
|
|
126
|
+
}, [updateSessionViewCache])
|
|
127
|
+
|
|
84
128
|
const clearScheduledReconciles = useCallback(() => {
|
|
85
129
|
for (const timer of reconcileTimersRef.current) {
|
|
86
130
|
clearTimeout(timer)
|
|
@@ -123,18 +167,17 @@ export function useChatSessionMessages({
|
|
|
123
167
|
res.session?.status
|
|
124
168
|
)
|
|
125
169
|
const latestFatalError = findLatestFatalError(events)
|
|
170
|
+
const nextErrorState = restoredInteraction == null && res.session?.status === 'failed' && latestFatalError != null
|
|
171
|
+
? {
|
|
172
|
+
kind: 'session' as const,
|
|
173
|
+
message: latestFatalError.message,
|
|
174
|
+
code: latestFatalError.code
|
|
175
|
+
}
|
|
176
|
+
: null
|
|
126
177
|
|
|
127
178
|
interactionRequestRef.current = restoredInteraction
|
|
128
179
|
setInteractionRequest(restoredInteraction)
|
|
129
|
-
setErrorState(
|
|
130
|
-
restoredInteraction == null && res.session?.status === 'failed' && latestFatalError != null
|
|
131
|
-
? {
|
|
132
|
-
kind: 'session',
|
|
133
|
-
message: latestFatalError.message,
|
|
134
|
-
code: latestFatalError.code
|
|
135
|
-
}
|
|
136
|
-
: null
|
|
137
|
-
)
|
|
180
|
+
setErrorState(nextErrorState)
|
|
138
181
|
|
|
139
182
|
for (const data of events) {
|
|
140
183
|
currentMessages = applyMessageEvent(currentMessages, data)
|
|
@@ -146,6 +189,14 @@ export function useChatSessionMessages({
|
|
|
146
189
|
}
|
|
147
190
|
}
|
|
148
191
|
|
|
192
|
+
updateSessionViewCache(sessionId, {
|
|
193
|
+
messages: currentMessages,
|
|
194
|
+
sessionInfo: currentSessionInfo,
|
|
195
|
+
errorState: nextErrorState,
|
|
196
|
+
interactionRequest: restoredInteraction,
|
|
197
|
+
isHydrated: true
|
|
198
|
+
})
|
|
199
|
+
|
|
149
200
|
setMessages(currentMessages)
|
|
150
201
|
setSessionInfo(currentSessionInfo)
|
|
151
202
|
|
|
@@ -161,7 +212,7 @@ export function useChatSessionMessages({
|
|
|
161
212
|
} catch (err) {
|
|
162
213
|
console.error('Failed to fetch history messages:', err)
|
|
163
214
|
}
|
|
164
|
-
}, [mutate, setInteractionRequest])
|
|
215
|
+
}, [mutate, setInteractionRequest, setMessages, updateSessionViewCache])
|
|
165
216
|
|
|
166
217
|
const reconcileAfterInteraction = useCallback(() => {
|
|
167
218
|
clearScheduledReconciles()
|
|
@@ -178,21 +229,20 @@ export function useChatSessionMessages({
|
|
|
178
229
|
if (session?.id == null || session.id === '') return
|
|
179
230
|
expectedCloseRef.current = true
|
|
180
231
|
setErrorState(null)
|
|
232
|
+
updateSessionViewCache(session.id, { errorState: null })
|
|
181
233
|
connectionManager.close(session.id)
|
|
182
234
|
setRetryCount((count) => count + 1)
|
|
183
|
-
}, [session?.id])
|
|
235
|
+
}, [session?.id, updateSessionViewCache])
|
|
184
236
|
|
|
185
237
|
useEffect(() => {
|
|
186
|
-
setMessages([])
|
|
187
|
-
setSessionInfo(null)
|
|
188
|
-
setIsReady(false)
|
|
189
|
-
setErrorState(null)
|
|
190
|
-
setInteractionRequest(null)
|
|
191
|
-
interactionRequestRef.current = null
|
|
192
|
-
isInitialLoadRef.current = true
|
|
193
|
-
|
|
194
238
|
if (session?.id == null || session.id === '') {
|
|
239
|
+
setMessagesState([])
|
|
240
|
+
setSessionInfo(null)
|
|
195
241
|
setIsReady(true)
|
|
242
|
+
setErrorState(null)
|
|
243
|
+
setInteractionRequest(null)
|
|
244
|
+
interactionRequestRef.current = null
|
|
245
|
+
isInitialLoadRef.current = true
|
|
196
246
|
lastConnectedModelRef.current = undefined
|
|
197
247
|
lastConnectedEffortRef.current = undefined
|
|
198
248
|
lastConnectedPermissionModeRef.current = undefined
|
|
@@ -201,6 +251,16 @@ export function useChatSessionMessages({
|
|
|
201
251
|
return
|
|
202
252
|
}
|
|
203
253
|
|
|
254
|
+
const restoredState = restoreChatSessionViewSnapshot(sessionViewCacheRef.current.get(session.id))
|
|
255
|
+
|
|
256
|
+
setMessagesState(restoredState.messages)
|
|
257
|
+
setSessionInfo(restoredState.sessionInfo)
|
|
258
|
+
setErrorState(restoredState.errorState)
|
|
259
|
+
setInteractionRequest(restoredState.interactionRequest)
|
|
260
|
+
interactionRequestRef.current = restoredState.interactionRequest
|
|
261
|
+
setIsReady(restoredState.isReady)
|
|
262
|
+
isInitialLoadRef.current = !restoredState.isReady
|
|
263
|
+
|
|
204
264
|
void refreshHistory()
|
|
205
265
|
|
|
206
266
|
return () => {
|
|
@@ -281,7 +341,13 @@ export function useChatSessionMessages({
|
|
|
281
341
|
cleanup = connectionManager.connect(session.id, {
|
|
282
342
|
onOpen() {
|
|
283
343
|
expectedCloseRef.current = false
|
|
284
|
-
setErrorState((current) =>
|
|
344
|
+
setErrorState((current) => {
|
|
345
|
+
const next = current?.kind === 'session' ? current : null
|
|
346
|
+
updateSessionViewCache(session.id, {
|
|
347
|
+
errorState: next
|
|
348
|
+
})
|
|
349
|
+
return next
|
|
350
|
+
})
|
|
285
351
|
},
|
|
286
352
|
onMessage(data: WSEvent) {
|
|
287
353
|
if (isDisposed) return
|
|
@@ -289,8 +355,14 @@ export function useChatSessionMessages({
|
|
|
289
355
|
if (nextInteraction !== interactionRequestRef.current) {
|
|
290
356
|
interactionRequestRef.current = nextInteraction
|
|
291
357
|
setInteractionRequest(nextInteraction)
|
|
358
|
+
updateSessionViewCache(session.id, {
|
|
359
|
+
interactionRequest: nextInteraction
|
|
360
|
+
})
|
|
292
361
|
if (nextInteraction != null) {
|
|
293
362
|
setErrorState(null)
|
|
363
|
+
updateSessionViewCache(session.id, {
|
|
364
|
+
errorState: null
|
|
365
|
+
})
|
|
294
366
|
}
|
|
295
367
|
}
|
|
296
368
|
if (data.type === 'interaction_response') {
|
|
@@ -300,10 +372,14 @@ export function useChatSessionMessages({
|
|
|
300
372
|
if (data.type === 'error') {
|
|
301
373
|
const fatalError = getFatalSessionError(data)
|
|
302
374
|
if (fatalError != null) {
|
|
303
|
-
|
|
375
|
+
const nextErrorState = {
|
|
304
376
|
kind: 'session',
|
|
305
377
|
message: fatalError.message,
|
|
306
378
|
code: fatalError.code
|
|
379
|
+
} satisfies ChatErrorState
|
|
380
|
+
setErrorState(nextErrorState)
|
|
381
|
+
updateSessionViewCache(session.id, {
|
|
382
|
+
errorState: nextErrorState
|
|
307
383
|
})
|
|
308
384
|
}
|
|
309
385
|
return
|
|
@@ -315,6 +391,7 @@ export function useChatSessionMessages({
|
|
|
315
391
|
const updatedSession = data.session as Session | { id: string; isDeleted: boolean }
|
|
316
392
|
|
|
317
393
|
if ('isDeleted' in updatedSession && updatedSession.isDeleted) {
|
|
394
|
+
removeSessionViewCache(updatedSession.id)
|
|
318
395
|
return {
|
|
319
396
|
...prev,
|
|
320
397
|
sessions: prev.sessions.filter((s: Session) => s.id !== updatedSession.id)
|
|
@@ -347,6 +424,9 @@ export function useChatSessionMessages({
|
|
|
347
424
|
void mutate('/api/sessions')
|
|
348
425
|
} else {
|
|
349
426
|
setSessionInfo(data.info ?? null)
|
|
427
|
+
updateSessionViewCache(session.id, {
|
|
428
|
+
sessionInfo: data.info ?? null
|
|
429
|
+
})
|
|
350
430
|
if (isInitialLoadRef.current) {
|
|
351
431
|
setTimeout(() => {
|
|
352
432
|
if (isDisposed) return
|
|
@@ -366,15 +446,23 @@ export function useChatSessionMessages({
|
|
|
366
446
|
}
|
|
367
447
|
|
|
368
448
|
if (data.type === 'interaction_request') {
|
|
449
|
+
interactionRequestRef.current = data
|
|
369
450
|
setInteractionRequest(data)
|
|
451
|
+
updateSessionViewCache(session.id, {
|
|
452
|
+
interactionRequest: data
|
|
453
|
+
})
|
|
370
454
|
}
|
|
371
455
|
},
|
|
372
456
|
onError() {
|
|
373
457
|
if (isDisposed) return
|
|
374
|
-
|
|
458
|
+
const nextErrorState = {
|
|
375
459
|
kind: 'connection',
|
|
376
460
|
message: t('chat.connectionError'),
|
|
377
461
|
reason: 'error'
|
|
462
|
+
} satisfies ChatErrorState
|
|
463
|
+
setErrorState(nextErrorState)
|
|
464
|
+
updateSessionViewCache(session.id, {
|
|
465
|
+
errorState: nextErrorState
|
|
378
466
|
})
|
|
379
467
|
},
|
|
380
468
|
onClose() {
|
|
@@ -383,13 +471,17 @@ export function useChatSessionMessages({
|
|
|
383
471
|
expectedCloseRef.current = false
|
|
384
472
|
return
|
|
385
473
|
}
|
|
386
|
-
setErrorState((current) =>
|
|
387
|
-
current ?? {
|
|
474
|
+
setErrorState((current) => {
|
|
475
|
+
const next = current ?? {
|
|
388
476
|
kind: 'connection',
|
|
389
477
|
message: t('chat.connectionClosed'),
|
|
390
478
|
reason: 'closed'
|
|
391
479
|
}
|
|
392
|
-
|
|
480
|
+
updateSessionViewCache(session.id, {
|
|
481
|
+
errorState: next
|
|
482
|
+
})
|
|
483
|
+
return next
|
|
484
|
+
})
|
|
393
485
|
}
|
|
394
486
|
}, Object.keys(connectionParams).length > 0 ? connectionParams : undefined)
|
|
395
487
|
}, (modelChanged || effortChanged || permissionModeChanged || adapterChanged) ? 200 : 100)
|
|
@@ -412,11 +504,13 @@ export function useChatSessionMessages({
|
|
|
412
504
|
session?.id,
|
|
413
505
|
session?.status,
|
|
414
506
|
setInteractionRequest,
|
|
415
|
-
t
|
|
507
|
+
t,
|
|
508
|
+
removeSessionViewCache,
|
|
509
|
+
updateSessionViewCache
|
|
416
510
|
])
|
|
417
511
|
|
|
418
512
|
return {
|
|
419
|
-
messages,
|
|
513
|
+
messages: messagesState,
|
|
420
514
|
setMessages,
|
|
421
515
|
sessionInfo,
|
|
422
516
|
isReady,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
"batchRestoreSuccess": "Batch restored successfully",
|
|
25
25
|
"batchRestoreFailed": "Failed to restore some sessions",
|
|
26
26
|
"cancel": "Cancel",
|
|
27
|
+
"continue": "Continue",
|
|
27
28
|
"confirm": "Confirm",
|
|
28
29
|
"confirmAction": "Confirm {{action}}",
|
|
29
30
|
"startNewChat": "No active sessions found. Start a new chat!",
|
|
@@ -438,6 +439,73 @@
|
|
|
438
439
|
"viewTimeline": "Timeline",
|
|
439
440
|
"viewTerminal": "Terminal",
|
|
440
441
|
"viewSettings": "Settings",
|
|
442
|
+
"gitBranchSwitcher": "Switch branch",
|
|
443
|
+
"gitOperations": "Git actions",
|
|
444
|
+
"gitWorktree": "Worktree",
|
|
445
|
+
"gitDetachedHead": "DETACHED",
|
|
446
|
+
"gitSearchBranches": "Search local or remote branches",
|
|
447
|
+
"gitBranchesLocal": "Local branches",
|
|
448
|
+
"gitBranchesWorktrees": "Other worktrees",
|
|
449
|
+
"gitBranchesRemote": "Remote branches",
|
|
450
|
+
"gitBranchCheckedOutInOtherWorktree": "Checked out in {{path}}",
|
|
451
|
+
"gitRemoteBranchCheckedOutInOtherWorktree": "Local branch already checked out in {{path}}",
|
|
452
|
+
"gitNoBranches": "No matching branches",
|
|
453
|
+
"gitCreateBranch": "Create branch",
|
|
454
|
+
"gitCreateBranchWithName": "Create branch {{branch}}",
|
|
455
|
+
"gitSwitchBranchSuccess": "Switched to {{branch}}",
|
|
456
|
+
"gitCreateBranchSuccess": "Created and switched to {{branch}}",
|
|
457
|
+
"gitCommitShort": "Commit",
|
|
458
|
+
"gitCommitAllChanges": "Commit all changes",
|
|
459
|
+
"gitCommitDialogTitle": "Commit all changes",
|
|
460
|
+
"gitCommitDescription": "This stages all current repository changes before committing.",
|
|
461
|
+
"gitCommitPanelTitle": "Commit changes",
|
|
462
|
+
"gitCommitPanelBranch": "Branch",
|
|
463
|
+
"gitCommitPanelChanges": "Changes",
|
|
464
|
+
"gitChangedFilesCount": "{{count}} files",
|
|
465
|
+
"gitCommitIncludeUnstagedChanges": "Include unstaged changes",
|
|
466
|
+
"gitCommitIncludeUnstagedChangesDescription": "Include unstaged and untracked files.",
|
|
467
|
+
"gitCommitOnlyStagedChangesDescription": "Commit staged changes only.",
|
|
468
|
+
"gitCommitSkipHooks": "Skip Git hooks",
|
|
469
|
+
"gitCommitSkipHooksDescription": "Skip pre-commit and commit-msg hooks.",
|
|
470
|
+
"gitCommitAmend": "Amend latest commit",
|
|
471
|
+
"gitCommitAmendDescription": "Fold into the latest commit. Current: {{subject}}",
|
|
472
|
+
"gitCommitAmendUnavailableDescription": "There is no existing commit to amend in this repository.",
|
|
473
|
+
"gitCommitMessageLabel": "Commit message",
|
|
474
|
+
"gitCommitMessageOptional": "Optional",
|
|
475
|
+
"gitCommitMessagePlaceholder": "Enter a commit message",
|
|
476
|
+
"gitCommitMessagePlaceholderAmend": "Leave blank to keep the latest commit message",
|
|
477
|
+
"gitCommitMessageAmendHint": "Leave it blank to reuse the latest commit message.",
|
|
478
|
+
"gitCommitMessageRequired": "Enter a commit message",
|
|
479
|
+
"gitCommitSuccess": "Commit created",
|
|
480
|
+
"gitCommitNoChanges": "There are no changes to commit",
|
|
481
|
+
"gitCommitNoStagedChanges": "There are no staged changes to commit",
|
|
482
|
+
"gitCommitNextStep": "Next step",
|
|
483
|
+
"gitCommitAndPush": "Commit and push",
|
|
484
|
+
"gitCommitAndPushSuccess": "Commit and push completed",
|
|
485
|
+
"gitCommitPushFailedAfterCommit": "Commit completed, but push failed: {{error}}",
|
|
486
|
+
"gitAmendSuccess": "Amend completed",
|
|
487
|
+
"gitAmendAndPushSuccess": "Amend and push completed",
|
|
488
|
+
"gitAmendUnavailable": "There is no commit available to amend",
|
|
489
|
+
"gitForcePush": "Force push",
|
|
490
|
+
"gitForcePushDescription": "Use force-with-lease for the remote branch.",
|
|
491
|
+
"gitForcePushHint": "Only replaces it when the remote has no newer commits.",
|
|
492
|
+
"gitPushNeedsSyncOrForce": "This branch is behind upstream. Sync first or enable force push.",
|
|
493
|
+
"gitPushShort": "Push",
|
|
494
|
+
"gitPush": "Push current branch",
|
|
495
|
+
"gitPushPanelTitle": "Push changes",
|
|
496
|
+
"gitPushPanelUpstream": "Upstream",
|
|
497
|
+
"gitPushPanelUpstreamHint": "The first push sets the upstream automatically.",
|
|
498
|
+
"gitForcePushSuccess": "Force push completed",
|
|
499
|
+
"gitPushSuccess": "Push completed",
|
|
500
|
+
"gitSyncShort": "Sync",
|
|
501
|
+
"gitSync": "Sync current branch",
|
|
502
|
+
"gitSyncSuccess": "Sync completed",
|
|
503
|
+
"gitStatusDirty": "Uncommitted changes",
|
|
504
|
+
"gitStatusClean": "Working tree clean",
|
|
505
|
+
"gitUpstreamStatus": "Ahead {{ahead}} / behind {{behind}}",
|
|
506
|
+
"gitNoUpstream": "No upstream configured",
|
|
507
|
+
"gitLocalBranch": "Local",
|
|
508
|
+
"gitRemoteBranch": "Remote · {{remote}}",
|
|
441
509
|
"deleteSessionTitle": "Delete session",
|
|
442
510
|
"deleteSessionDesc": "This will permanently remove the session and all messages. Proceed carefully.",
|
|
443
511
|
"timelineEmpty": "No events yet",
|