mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
feat(webui): add session workspace rail (#92856)
* feat(webui): add session workspace rail * fix(webui): address session workspace review * fix(webui): secure session workspace previews * fix(webui): handle nested session workspace paths * fix(webui): update session file protocol models * fix(webui): clear session rail lint
This commit is contained in:
@@ -2074,6 +2074,204 @@ public struct SessionsCompactionRestoreResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileBrowserEntry: Codable, Sendable {
|
||||
public let path: String
|
||||
public let name: String
|
||||
public let kind: AnyCodable
|
||||
public let sessionkind: SessionFileRelevance?
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
name: String,
|
||||
kind: AnyCodable,
|
||||
sessionkind: SessionFileRelevance?,
|
||||
size: Int?,
|
||||
updatedatms: Int?)
|
||||
{
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.sessionkind = sessionkind
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case name
|
||||
case kind
|
||||
case sessionkind = "sessionKind"
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileBrowserResult: Codable, Sendable {
|
||||
public let path: String
|
||||
public let parentpath: String?
|
||||
public let search: String?
|
||||
public let entries: [SessionFileBrowserEntry]
|
||||
public let truncated: Bool?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
parentpath: String?,
|
||||
search: String?,
|
||||
entries: [SessionFileBrowserEntry],
|
||||
truncated: Bool?)
|
||||
{
|
||||
self.path = path
|
||||
self.parentpath = parentpath
|
||||
self.search = search
|
||||
self.entries = entries
|
||||
self.truncated = truncated
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case parentpath = "parentPath"
|
||||
case search
|
||||
case entries
|
||||
case truncated
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionFileEntry: Codable, Sendable {
|
||||
public let path: String
|
||||
public let name: String
|
||||
public let kind: SessionFileKind
|
||||
public let missing: Bool
|
||||
public let size: Int?
|
||||
public let updatedatms: Int?
|
||||
public let content: String?
|
||||
|
||||
public init(
|
||||
path: String,
|
||||
name: String,
|
||||
kind: SessionFileKind,
|
||||
missing: Bool,
|
||||
size: Int?,
|
||||
updatedatms: Int?,
|
||||
content: String?)
|
||||
{
|
||||
self.path = path
|
||||
self.name = name
|
||||
self.kind = kind
|
||||
self.missing = missing
|
||||
self.size = size
|
||||
self.updatedatms = updatedatms
|
||||
self.content = content
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case path
|
||||
case name
|
||||
case kind
|
||||
case missing
|
||||
case size
|
||||
case updatedatms = "updatedAtMs"
|
||||
case content
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesListParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let agentid: String?
|
||||
public let path: String?
|
||||
public let search: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
agentid: String? = nil,
|
||||
path: String?,
|
||||
search: String?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.agentid = agentid
|
||||
self.path = path
|
||||
self.search = search
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case agentid = "agentId"
|
||||
case path
|
||||
case search
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesListResult: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let root: String?
|
||||
public let files: [SessionFileEntry]
|
||||
public let browser: SessionFileBrowserResult?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
root: String?,
|
||||
files: [SessionFileEntry],
|
||||
browser: SessionFileBrowserResult?)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.root = root
|
||||
self.files = files
|
||||
self.browser = browser
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case root
|
||||
case files
|
||||
case browser
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesGetParams: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let path: String
|
||||
public let agentid: String?
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
path: String,
|
||||
agentid: String? = nil)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.path = path
|
||||
self.agentid = agentid
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case path
|
||||
case agentid = "agentId"
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsFilesGetResult: Codable, Sendable {
|
||||
public let sessionkey: String
|
||||
public let root: String?
|
||||
public let file: SessionFileEntry
|
||||
|
||||
public init(
|
||||
sessionkey: String,
|
||||
root: String?,
|
||||
file: SessionFileEntry)
|
||||
{
|
||||
self.sessionkey = sessionkey
|
||||
self.root = root
|
||||
self.file = file
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case sessionkey = "sessionKey"
|
||||
case root
|
||||
case file
|
||||
}
|
||||
}
|
||||
|
||||
public struct SessionsCreateParams: Codable, Sendable {
|
||||
public let key: String?
|
||||
public let agentid: String?
|
||||
|
||||
@@ -321,6 +321,16 @@ import {
|
||||
SessionsCompactionListParamsSchema,
|
||||
type SessionsCompactionRestoreParams,
|
||||
SessionsCompactionRestoreParamsSchema,
|
||||
type SessionFileBrowserEntry,
|
||||
SessionFileBrowserEntrySchema,
|
||||
type SessionFileBrowserResult,
|
||||
SessionFileBrowserResultSchema,
|
||||
type SessionFileEntry,
|
||||
SessionFileEntrySchema,
|
||||
type SessionFileKind,
|
||||
SessionFileKindSchema,
|
||||
type SessionFileRelevance,
|
||||
SessionFileRelevanceSchema,
|
||||
type SessionOperationEvent,
|
||||
type SessionsCreateParams,
|
||||
SessionsCreateParamsSchema,
|
||||
@@ -328,6 +338,14 @@ import {
|
||||
SessionsDeleteParamsSchema,
|
||||
type SessionsDescribeParams,
|
||||
SessionsDescribeParamsSchema,
|
||||
type SessionsFilesGetParams,
|
||||
SessionsFilesGetParamsSchema,
|
||||
type SessionsFilesGetResult,
|
||||
SessionsFilesGetResultSchema,
|
||||
type SessionsFilesListParams,
|
||||
SessionsFilesListParamsSchema,
|
||||
type SessionsFilesListResult,
|
||||
SessionsFilesListResultSchema,
|
||||
type SessionsListParams,
|
||||
SessionsListParamsSchema,
|
||||
type SessionsMessagesSubscribeParams,
|
||||
@@ -630,6 +648,12 @@ export const validateSessionsDescribeParams = lazyCompile<SessionsDescribeParams
|
||||
export const validateSessionsResolveParams = lazyCompile<SessionsResolveParams>(
|
||||
SessionsResolveParamsSchema,
|
||||
);
|
||||
export const validateSessionsFilesListParams = lazyCompile<SessionsFilesListParams>(
|
||||
SessionsFilesListParamsSchema,
|
||||
);
|
||||
export const validateSessionsFilesGetParams = lazyCompile<SessionsFilesGetParams>(
|
||||
SessionsFilesGetParamsSchema,
|
||||
);
|
||||
export const validateSessionsCreateParams = lazyCompile<SessionsCreateParams>(
|
||||
SessionsCreateParamsSchema,
|
||||
);
|
||||
@@ -1005,6 +1029,15 @@ export {
|
||||
SessionsPreviewParamsSchema,
|
||||
SessionsDescribeParamsSchema,
|
||||
SessionsResolveParamsSchema,
|
||||
SessionFileBrowserEntrySchema,
|
||||
SessionFileBrowserResultSchema,
|
||||
SessionFileEntrySchema,
|
||||
SessionFileKindSchema,
|
||||
SessionFileRelevanceSchema,
|
||||
SessionsFilesGetParamsSchema,
|
||||
SessionsFilesGetResultSchema,
|
||||
SessionsFilesListParamsSchema,
|
||||
SessionsFilesListResultSchema,
|
||||
SessionsCompactionListParamsSchema,
|
||||
SessionsCompactionGetParamsSchema,
|
||||
SessionsCompactionBranchParamsSchema,
|
||||
@@ -1248,6 +1281,15 @@ export type {
|
||||
AgentsFilesGetResult,
|
||||
AgentsFilesSetParams,
|
||||
AgentsFilesSetResult,
|
||||
SessionFileBrowserEntry,
|
||||
SessionFileBrowserResult,
|
||||
SessionFileEntry,
|
||||
SessionFileKind,
|
||||
SessionFileRelevance,
|
||||
SessionsFilesListParams,
|
||||
SessionsFilesListResult,
|
||||
SessionsFilesGetParams,
|
||||
SessionsFilesGetResult,
|
||||
ArtifactSummary,
|
||||
ArtifactsListParams,
|
||||
ArtifactsListResult,
|
||||
|
||||
@@ -262,12 +262,21 @@ import {
|
||||
SessionsCompactionListResultSchema,
|
||||
SessionsCompactionRestoreParamsSchema,
|
||||
SessionsCompactionRestoreResultSchema,
|
||||
SessionFileBrowserEntrySchema,
|
||||
SessionFileBrowserResultSchema,
|
||||
SessionCompactionCheckpointSchema,
|
||||
SessionFileEntrySchema,
|
||||
SessionFileKindSchema,
|
||||
SessionFileRelevanceSchema,
|
||||
SessionOperationEventSchema,
|
||||
SessionsCleanupParamsSchema,
|
||||
SessionsCreateParamsSchema,
|
||||
SessionsDeleteParamsSchema,
|
||||
SessionsDescribeParamsSchema,
|
||||
SessionsFilesGetParamsSchema,
|
||||
SessionsFilesGetResultSchema,
|
||||
SessionsFilesListParamsSchema,
|
||||
SessionsFilesListResultSchema,
|
||||
SessionsListParamsSchema,
|
||||
SessionsMessagesSubscribeParamsSchema,
|
||||
SessionsMessagesUnsubscribeParamsSchema,
|
||||
@@ -379,6 +388,15 @@ export const ProtocolSchemas = {
|
||||
SessionsCompactionGetResult: SessionsCompactionGetResultSchema,
|
||||
SessionsCompactionBranchResult: SessionsCompactionBranchResultSchema,
|
||||
SessionsCompactionRestoreResult: SessionsCompactionRestoreResultSchema,
|
||||
SessionFileBrowserEntry: SessionFileBrowserEntrySchema,
|
||||
SessionFileBrowserResult: SessionFileBrowserResultSchema,
|
||||
SessionFileKind: SessionFileKindSchema,
|
||||
SessionFileEntry: SessionFileEntrySchema,
|
||||
SessionFileRelevance: SessionFileRelevanceSchema,
|
||||
SessionsFilesListParams: SessionsFilesListParamsSchema,
|
||||
SessionsFilesListResult: SessionsFilesListResultSchema,
|
||||
SessionsFilesGetParams: SessionsFilesGetParamsSchema,
|
||||
SessionsFilesGetResult: SessionsFilesGetResultSchema,
|
||||
SessionsCreateParams: SessionsCreateParamsSchema,
|
||||
SessionsSendParams: SessionsSendParamsSchema,
|
||||
SessionsMessagesSubscribeParams: SessionsMessagesSubscribeParamsSchema,
|
||||
|
||||
@@ -63,6 +63,97 @@ export const SessionCompactionCheckpointSchema = Type.Object(
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Session file grouping used by the Control UI session workspace rail. */
|
||||
export const SessionFileKindSchema = Type.Union([Type.Literal("modified"), Type.Literal("read")]);
|
||||
|
||||
/** Session relevance marker for browser entries. */
|
||||
export const SessionFileRelevanceSchema = Type.Union([
|
||||
Type.Literal("modified"),
|
||||
Type.Literal("read"),
|
||||
Type.Literal("mixed"),
|
||||
]);
|
||||
|
||||
/** One file path referenced by a session transcript. */
|
||||
export const SessionFileEntrySchema = Type.Object(
|
||||
{
|
||||
path: NonEmptyString,
|
||||
name: NonEmptyString,
|
||||
kind: SessionFileKindSchema,
|
||||
missing: Type.Boolean(),
|
||||
size: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
content: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** One file or folder in the session-rooted browser. */
|
||||
export const SessionFileBrowserEntrySchema = Type.Object(
|
||||
{
|
||||
path: Type.String(),
|
||||
name: NonEmptyString,
|
||||
kind: Type.Union([Type.Literal("file"), Type.Literal("directory")]),
|
||||
sessionKind: Type.Optional(SessionFileRelevanceSchema),
|
||||
size: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
updatedAtMs: Type.Optional(Type.Integer({ minimum: 0 })),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Folder listing or search result rooted at the session workspace. */
|
||||
export const SessionFileBrowserResultSchema = Type.Object(
|
||||
{
|
||||
path: Type.String(),
|
||||
parentPath: Type.Optional(Type.String()),
|
||||
search: Type.Optional(Type.String()),
|
||||
entries: Type.Array(SessionFileBrowserEntrySchema),
|
||||
truncated: Type.Optional(Type.Boolean()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists files touched by a session transcript. */
|
||||
export const SessionsFilesListParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
path: Type.Optional(Type.String()),
|
||||
search: Type.Optional(Type.String()),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** File references visible in one session workspace. */
|
||||
export const SessionsFilesListResultSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
root: Type.Optional(NonEmptyString),
|
||||
files: Type.Array(SessionFileEntrySchema),
|
||||
browser: Type.Optional(SessionFileBrowserResultSchema),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Reads one session-referenced file by path. */
|
||||
export const SessionsFilesGetParamsSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
path: NonEmptyString,
|
||||
agentId: Type.Optional(NonEmptyString),
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Result for reading one session-referenced file. */
|
||||
export const SessionsFilesGetResultSchema = Type.Object(
|
||||
{
|
||||
sessionKey: NonEmptyString,
|
||||
root: Type.Optional(NonEmptyString),
|
||||
file: SessionFileEntrySchema,
|
||||
},
|
||||
{ additionalProperties: false },
|
||||
);
|
||||
|
||||
/** Lists sessions with optional scope, activity, label, and preview filters. */
|
||||
export const SessionsListParamsSchema = Type.Object(
|
||||
{
|
||||
|
||||
@@ -179,6 +179,15 @@ export type AgentsFilesGetParams = SchemaType<"AgentsFilesGetParams">;
|
||||
export type AgentsFilesGetResult = SchemaType<"AgentsFilesGetResult">;
|
||||
export type AgentsFilesSetParams = SchemaType<"AgentsFilesSetParams">;
|
||||
export type AgentsFilesSetResult = SchemaType<"AgentsFilesSetResult">;
|
||||
export type SessionFileKind = SchemaType<"SessionFileKind">;
|
||||
export type SessionFileRelevance = SchemaType<"SessionFileRelevance">;
|
||||
export type SessionFileEntry = SchemaType<"SessionFileEntry">;
|
||||
export type SessionFileBrowserEntry = SchemaType<"SessionFileBrowserEntry">;
|
||||
export type SessionFileBrowserResult = SchemaType<"SessionFileBrowserResult">;
|
||||
export type SessionsFilesListParams = SchemaType<"SessionsFilesListParams">;
|
||||
export type SessionsFilesListResult = SchemaType<"SessionsFilesListResult">;
|
||||
export type SessionsFilesGetParams = SchemaType<"SessionsFilesGetParams">;
|
||||
export type SessionsFilesGetResult = SchemaType<"SessionsFilesGetResult">;
|
||||
export type ArtifactSummary = SchemaType<"ArtifactSummary">;
|
||||
export type ArtifactsListParams = SchemaType<"ArtifactsListParams">;
|
||||
export type ArtifactsListResult = SchemaType<"ArtifactsListResult">;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "../ui/vite.config.ts";
|
||||
|
||||
type CliOptions = {
|
||||
allowedHosts: string[];
|
||||
host: string;
|
||||
port: number;
|
||||
};
|
||||
@@ -33,10 +34,20 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."
|
||||
const uiRoot = path.join(repoRoot, "ui");
|
||||
|
||||
function parseArgs(args: string[]): CliOptions {
|
||||
const options: CliOptions = { host: "127.0.0.1", port: 5187 };
|
||||
const options: CliOptions = { allowedHosts: [], host: "127.0.0.1", port: 5187 };
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
if (arg === "--host") {
|
||||
if (arg === "--allowed-host") {
|
||||
const allowedHost = args[++i]?.trim();
|
||||
if (allowedHost) {
|
||||
options.allowedHosts.push(allowedHost);
|
||||
}
|
||||
} else if (arg.startsWith("--allowed-host=")) {
|
||||
const allowedHost = arg.slice("--allowed-host=".length).trim();
|
||||
if (allowedHost) {
|
||||
options.allowedHosts.push(allowedHost);
|
||||
}
|
||||
} else if (arg === "--host") {
|
||||
options.host = args[++i] ?? options.host;
|
||||
} else if (arg.startsWith("--host=")) {
|
||||
options.host = arg.slice("--host=".length) || options.host;
|
||||
@@ -253,6 +264,146 @@ function createChatPickerScenario(): ControlUiMockGatewayScenario {
|
||||
},
|
||||
})),
|
||||
);
|
||||
const sessionFiles = [
|
||||
{
|
||||
kind: "modified",
|
||||
missing: false,
|
||||
name: "chat.ts",
|
||||
path: "ui/src/ui/views/chat.ts",
|
||||
size: 48320,
|
||||
updatedAtMs: baseTime - 20_000,
|
||||
},
|
||||
{
|
||||
kind: "modified",
|
||||
missing: false,
|
||||
name: "sidebar.css",
|
||||
path: "ui/src/styles/chat/sidebar.css",
|
||||
size: 18840,
|
||||
updatedAtMs: baseTime - 18_000,
|
||||
},
|
||||
{
|
||||
kind: "read",
|
||||
missing: false,
|
||||
name: "artifacts.ts",
|
||||
path: "src/gateway/server-methods/artifacts.ts",
|
||||
size: 21876,
|
||||
updatedAtMs: baseTime - 300_000,
|
||||
},
|
||||
{
|
||||
kind: "read",
|
||||
missing: false,
|
||||
name: "sessions.ts",
|
||||
path: "packages/gateway-protocol/src/schema/sessions.ts",
|
||||
size: 16542,
|
||||
updatedAtMs: baseTime - 420_000,
|
||||
},
|
||||
];
|
||||
const sessionWorkspaceRoot = repoRoot;
|
||||
const sessionFileContentByPath = new Map([
|
||||
[
|
||||
"ui/src/ui/views/chat.ts",
|
||||
'function renderSessionWorkspaceRail() {\n return html`<aside class="chat-workspace-rail">...</aside>`;\n}\n',
|
||||
],
|
||||
[
|
||||
"ui/src/styles/chat/sidebar.css",
|
||||
".chat-workspace-rail__section-title {\n color: var(--muted);\n text-transform: uppercase;\n}\n",
|
||||
],
|
||||
[
|
||||
"src/gateway/server-methods/artifacts.ts",
|
||||
"// Artifact gateway methods collect generated artifacts from session transcripts.\n",
|
||||
],
|
||||
[
|
||||
"packages/gateway-protocol/src/schema/sessions.ts",
|
||||
"export const SessionsFilesListParamsSchema = Type.Object({ sessionKey: NonEmptyString });\n",
|
||||
],
|
||||
[
|
||||
"package.json",
|
||||
'{\n "name": "openclaw",\n "scripts": { "dev:ui:mock": "tsx scripts/control-ui-mock-dev.ts" }\n}\n',
|
||||
],
|
||||
[
|
||||
"ui/vite.config.ts",
|
||||
"export default function controlUiViteConfig() {\n return { server: { strictPort: true } };\n}\n",
|
||||
],
|
||||
[
|
||||
"ui/src/ui/e2e/chat-flow.e2e.test.ts",
|
||||
"it('keeps the session workspace useful while browsing files', async () => {\n await page.getByText('Project files').waitFor();\n});\n",
|
||||
],
|
||||
]);
|
||||
const sessionFileCases = [
|
||||
{
|
||||
match: { sessionKey: "agent:alpha" },
|
||||
response: {
|
||||
browser: {
|
||||
entries: [
|
||||
{
|
||||
kind: "directory",
|
||||
name: "packages",
|
||||
path: "packages",
|
||||
sessionKind: "read",
|
||||
updatedAtMs: baseTime - 420_000,
|
||||
},
|
||||
{
|
||||
kind: "directory",
|
||||
name: "src",
|
||||
path: "src",
|
||||
sessionKind: "read",
|
||||
updatedAtMs: baseTime - 300_000,
|
||||
},
|
||||
{
|
||||
kind: "directory",
|
||||
name: "ui",
|
||||
path: "ui",
|
||||
sessionKind: "modified",
|
||||
updatedAtMs: baseTime - 20_000,
|
||||
},
|
||||
{
|
||||
kind: "file",
|
||||
name: "package.json",
|
||||
path: "package.json",
|
||||
size: 92750,
|
||||
updatedAtMs: baseTime - 800_000,
|
||||
},
|
||||
],
|
||||
path: "",
|
||||
},
|
||||
files: sessionFiles,
|
||||
root: sessionWorkspaceRoot,
|
||||
sessionKey: "agent:alpha",
|
||||
},
|
||||
},
|
||||
];
|
||||
const sessionFileGetCases = sessionFiles.map((file) => ({
|
||||
match: { sessionKey: "agent:alpha", path: file.path },
|
||||
response: {
|
||||
file: {
|
||||
...file,
|
||||
content: sessionFileContentByPath.get(file.path) ?? "",
|
||||
},
|
||||
root: sessionWorkspaceRoot,
|
||||
sessionKey: "agent:alpha",
|
||||
},
|
||||
}));
|
||||
const lobsterSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 360">
|
||||
<rect width="640" height="360" fill="#10151d"/>
|
||||
<circle cx="320" cy="185" r="76" fill="#e23f3f"/>
|
||||
<ellipse cx="250" cy="178" rx="54" ry="38" fill="#f05a52"/>
|
||||
<ellipse cx="390" cy="178" rx="54" ry="38" fill="#f05a52"/>
|
||||
<circle cx="292" cy="145" r="10" fill="#0b0f14"/>
|
||||
<circle cx="348" cy="145" r="10" fill="#0b0f14"/>
|
||||
<path d="M232 114c-72-44-135-22-146 35 52 9 91-4 125-39" fill="none" stroke="#f06b5f" stroke-width="28" stroke-linecap="round"/>
|
||||
<path d="M408 114c72-44 135-22 146 35-52 9-91-4-125-39" fill="none" stroke="#f06b5f" stroke-width="28" stroke-linecap="round"/>
|
||||
<path d="M232 246c-45 28-91 35-142 23M408 246c45 28 91 35 142 23" fill="none" stroke="#e14b47" stroke-width="16" stroke-linecap="round"/>
|
||||
<text x="320" y="326" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="24" fill="#f6f7f9">openclaw session artifact</text>
|
||||
</svg>`;
|
||||
const lobsterArtifact = {
|
||||
id: "artifact-openclaw-lobster",
|
||||
type: "image",
|
||||
title: "openclaw-lobster-preview.svg",
|
||||
mimeType: "image/svg+xml",
|
||||
sizeBytes: Buffer.byteLength(lobsterSvg, "utf8"),
|
||||
source: "session-transcript",
|
||||
download: { mode: "bytes" },
|
||||
};
|
||||
const sessions = [
|
||||
sessionRow("agent:alpha", "Alpha planning", baseTime - 1_000),
|
||||
...buildSessionRows({
|
||||
@@ -288,6 +439,91 @@ function createChatPickerScenario(): ControlUiMockGatewayScenario {
|
||||
"agents.files.list": {
|
||||
cases: workspaceListCases,
|
||||
},
|
||||
"sessions.files.get": {
|
||||
cases: sessionFileGetCases,
|
||||
},
|
||||
"sessions.files.list": {
|
||||
cases: [
|
||||
{
|
||||
match: { sessionKey: "agent:alpha", path: "ui" },
|
||||
response: {
|
||||
browser: {
|
||||
entries: [
|
||||
{
|
||||
kind: "directory",
|
||||
name: "src",
|
||||
path: "ui/src",
|
||||
sessionKind: "modified",
|
||||
updatedAtMs: baseTime - 20_000,
|
||||
},
|
||||
{
|
||||
kind: "file",
|
||||
name: "vite.config.ts",
|
||||
path: "ui/vite.config.ts",
|
||||
size: 9860,
|
||||
updatedAtMs: baseTime - 900_000,
|
||||
},
|
||||
],
|
||||
parentPath: "",
|
||||
path: "ui",
|
||||
},
|
||||
files: sessionFiles,
|
||||
root: sessionWorkspaceRoot,
|
||||
sessionKey: "agent:alpha",
|
||||
},
|
||||
},
|
||||
{
|
||||
match: { sessionKey: "agent:alpha", search: "chat" },
|
||||
response: {
|
||||
browser: {
|
||||
entries: [
|
||||
{
|
||||
kind: "file",
|
||||
name: "chat.ts",
|
||||
path: "ui/src/ui/views/chat.ts",
|
||||
sessionKind: "modified",
|
||||
size: 48320,
|
||||
updatedAtMs: baseTime - 20_000,
|
||||
},
|
||||
{
|
||||
kind: "file",
|
||||
name: "chat-flow.e2e.test.ts",
|
||||
path: "ui/src/ui/e2e/chat-flow.e2e.test.ts",
|
||||
size: 24950,
|
||||
updatedAtMs: baseTime - 25_000,
|
||||
},
|
||||
],
|
||||
path: "",
|
||||
search: "chat",
|
||||
},
|
||||
files: sessionFiles,
|
||||
root: sessionWorkspaceRoot,
|
||||
sessionKey: "agent:alpha",
|
||||
},
|
||||
},
|
||||
...sessionFileCases,
|
||||
],
|
||||
},
|
||||
"artifacts.list": {
|
||||
cases: [
|
||||
{
|
||||
match: { sessionKey: "agent:alpha" },
|
||||
response: { artifacts: [lobsterArtifact] },
|
||||
},
|
||||
],
|
||||
},
|
||||
"artifacts.download": {
|
||||
cases: [
|
||||
{
|
||||
match: { sessionKey: "agent:alpha", artifactId: lobsterArtifact.id },
|
||||
response: {
|
||||
artifact: lobsterArtifact,
|
||||
data: Buffer.from(lobsterSvg, "utf8").toString("base64"),
|
||||
encoding: "base64",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"sessions.list": {
|
||||
cases: [
|
||||
...buildSearchSessionListCases(telegramSessions, searchPrefixes("telegram")),
|
||||
@@ -375,9 +611,10 @@ const server = await createServer({
|
||||
},
|
||||
root: uiRoot,
|
||||
server: {
|
||||
allowedHosts: options.allowedHosts,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
strictPort: false,
|
||||
strictPort: true,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -106,6 +106,8 @@ export const CORE_GATEWAY_METHOD_SPECS: readonly CoreGatewayMethodSpec[] = [
|
||||
{ name: "agents.files.list", scope: "operator.read" },
|
||||
{ name: "agents.files.get", scope: "operator.read" },
|
||||
{ name: "agents.files.set", scope: "operator.admin" },
|
||||
{ name: "sessions.files.list", scope: "operator.read" },
|
||||
{ name: "sessions.files.get", scope: "operator.read" },
|
||||
{ name: "artifacts.list", scope: "operator.read" },
|
||||
{ name: "artifacts.get", scope: "operator.read" },
|
||||
{ name: "artifacts.download", scope: "operator.read" },
|
||||
|
||||
@@ -157,6 +157,10 @@ const loadSendHandlers = lazyHandlerModule(
|
||||
() => import("./server-methods/send.js"),
|
||||
(module) => module.sendHandlers,
|
||||
);
|
||||
const loadSessionsFilesHandlers = lazyHandlerModule(
|
||||
() => import("./server-methods/sessions-files.js"),
|
||||
(module) => module.sessionsFilesHandlers,
|
||||
);
|
||||
const loadSessionsHandlers = lazyHandlerModule(
|
||||
() => import("./server-methods/sessions.js"),
|
||||
(module) => module.sessionsHandlers,
|
||||
@@ -575,6 +579,10 @@ export const coreGatewayHandlers: GatewayRequestHandlers = {
|
||||
methods: ["artifacts.list", "artifacts.get", "artifacts.download"],
|
||||
loadHandlers: loadArtifactsHandlers,
|
||||
}),
|
||||
...createLazyCoreHandlers({
|
||||
methods: ["sessions.files.list", "sessions.files.get"],
|
||||
loadHandlers: loadSessionsFilesHandlers,
|
||||
}),
|
||||
};
|
||||
|
||||
/** Builds the per-request method registry from core, plugin, and explicit extra handlers. */
|
||||
|
||||
@@ -0,0 +1,633 @@
|
||||
// Session file method tests cover transcript-linked files plus the workspace browser.
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { sessionsFilesHandlers } from "./sessions-files.js";
|
||||
|
||||
const hoisted = vi.hoisted(() => ({
|
||||
loadSessionEntry: vi.fn(),
|
||||
resolveAgentWorkspaceDir: vi.fn(),
|
||||
resolveDefaultAgentId: vi.fn(),
|
||||
visitSessionMessagesAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentWorkspaceDir: hoisted.resolveAgentWorkspaceDir,
|
||||
resolveDefaultAgentId: hoisted.resolveDefaultAgentId,
|
||||
}));
|
||||
|
||||
vi.mock("../session-utils.js", async () => {
|
||||
const actual = await vi.importActual<typeof import("../session-utils.js")>("../session-utils.js");
|
||||
return {
|
||||
...actual,
|
||||
loadSessionEntry: hoisted.loadSessionEntry,
|
||||
visitSessionMessagesAsync: hoisted.visitSessionMessagesAsync,
|
||||
};
|
||||
});
|
||||
|
||||
function createResponder() {
|
||||
const calls: Array<{ ok: boolean; payload?: unknown; error?: unknown }> = [];
|
||||
return {
|
||||
calls,
|
||||
respond: (ok: boolean, payload?: unknown, error?: unknown) => {
|
||||
calls.push({ ok, payload, error });
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
type SessionFilesMethod = "sessions.files.list" | "sessions.files.get";
|
||||
|
||||
async function invokeSessionFilesHandler(
|
||||
method: SessionFilesMethod,
|
||||
params: Record<string, unknown>,
|
||||
) {
|
||||
const responder = createResponder();
|
||||
await sessionsFilesHandlers[method]?.({
|
||||
req: { type: "req", id: method, method, params: {} },
|
||||
params,
|
||||
client: null,
|
||||
isWebchatConnect: () => false,
|
||||
respond: responder.respond,
|
||||
context: {} as never,
|
||||
});
|
||||
return responder.calls;
|
||||
}
|
||||
|
||||
function expectOkPayload(calls: ReturnType<typeof createResponder>["calls"]): Record<string, any> {
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.ok).toBe(true);
|
||||
return calls[0]?.payload as Record<string, any>;
|
||||
}
|
||||
|
||||
function expectError(calls: ReturnType<typeof createResponder>["calls"]): Record<string, any> {
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]?.ok).toBe(false);
|
||||
return calls[0]?.error as Record<string, any>;
|
||||
}
|
||||
|
||||
function assistantToolCall(name: string, args: Record<string, unknown>) {
|
||||
return {
|
||||
role: "assistant",
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
name,
|
||||
arguments: args,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function writeWorkspaceFile(root: string, filePath: string, content: string) {
|
||||
const resolved = path.join(root, filePath);
|
||||
fs.mkdirSync(path.dirname(resolved), { recursive: true });
|
||||
fs.writeFileSync(resolved, content, "utf8");
|
||||
}
|
||||
|
||||
describe("sessions.files RPC handlers", () => {
|
||||
let workspaceRoot: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
workspaceRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-session-files-test-"));
|
||||
hoisted.resolveDefaultAgentId.mockReturnValue("main");
|
||||
hoisted.resolveAgentWorkspaceDir.mockReturnValue(workspaceRoot);
|
||||
writeWorkspaceFile(workspaceRoot, "package.json", '{"name":"openclaw-test"}\n');
|
||||
writeWorkspaceFile(workspaceRoot, "src/readme.md", "# Read me\n");
|
||||
writeWorkspaceFile(workspaceRoot, "ui/chat.ts", "export const chat = true;\n");
|
||||
writeWorkspaceFile(workspaceRoot, "ui/vite.config.ts", "export default {};\n");
|
||||
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:main:main",
|
||||
cfg: {},
|
||||
storePath: path.join(workspaceRoot, ".sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "sess-main.jsonl",
|
||||
spawnedCwd: workspaceRoot,
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
[
|
||||
assistantToolCall("edit", { path: "ui/chat.ts" }),
|
||||
assistantToolCall("read", { path: "src/readme.md" }),
|
||||
assistantToolCall("apply_patch", {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
}),
|
||||
].forEach((message, index) => visit(message, index + 1));
|
||||
return 3;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(workspaceRoot, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("lists session-touched files with a browser rooted at the session workspace", async () => {
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.root).toBe(workspaceRoot);
|
||||
expect(payload.files.map((file: Record<string, unknown>) => [file.path, file.kind])).toEqual([
|
||||
["package.json", "modified"],
|
||||
["ui/chat.ts", "modified"],
|
||||
["src/readme.md", "read"],
|
||||
]);
|
||||
expect(payload.browser.path).toBe("");
|
||||
expect(
|
||||
payload.browser.entries.map((entry: Record<string, unknown>) => [
|
||||
entry.path,
|
||||
entry.kind,
|
||||
entry.sessionKind,
|
||||
]),
|
||||
).toEqual([
|
||||
["src", "directory", "read"],
|
||||
["ui", "directory", "modified"],
|
||||
["package.json", "file", "modified"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("collects touched files from existing transcript tool-call spellings", async () => {
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "tool_use", name: "read", input: { path: "src/readme.md" } },
|
||||
{ type: "toolcall", name: "edit", arguments: { path: "ui/vite.config.ts" } },
|
||||
{ type: "tool_use", name: "read", args: { path: "ui/chat.ts" } },
|
||||
{
|
||||
type: "tool_call",
|
||||
name: "apply_patch",
|
||||
input: {
|
||||
input: "*** Begin Patch\n*** Update File: package.json\n*** End Patch\n",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.files.map((file: Record<string, unknown>) => [file.path, file.kind])).toEqual([
|
||||
["package.json", "modified"],
|
||||
["ui/vite.config.ts", "modified"],
|
||||
["src/readme.md", "read"],
|
||||
["ui/chat.ts", "read"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("collects changed files from structured apply_patch changes", async () => {
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(
|
||||
assistantToolCall("apply_patch", {
|
||||
changes: [
|
||||
{ path: "ui/chat.ts", kind: "update" },
|
||||
{ path: "src/readme.md", kind: "delete" },
|
||||
{ path: "old-name.md", kind: { type: "update", move_path: "package.json" } },
|
||||
],
|
||||
}),
|
||||
1,
|
||||
);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.files.map((file: Record<string, unknown>) => [file.path, file.kind])).toEqual([
|
||||
["old-name.md", "modified"],
|
||||
["package.json", "modified"],
|
||||
["src/readme.md", "modified"],
|
||||
["ui/chat.ts", "modified"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers the spawned workspace root over a nested spawned cwd", async () => {
|
||||
const nestedCwd = path.join(workspaceRoot, "packages/app");
|
||||
fs.mkdirSync(nestedCwd, { recursive: true });
|
||||
writeWorkspaceFile(workspaceRoot, "packages/app/src/readme.md", "# Nested read me\n");
|
||||
writeWorkspaceFile(workspaceRoot, "packages/shared/config.ts", "export const shared = true;\n");
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:main:main",
|
||||
cfg: {},
|
||||
storePath: path.join(workspaceRoot, ".sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "sess-main.jsonl",
|
||||
spawnedCwd: nestedCwd,
|
||||
spawnedWorkspaceDir: workspaceRoot,
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
visit(assistantToolCall("read", { path: "../shared/config.ts" }), 2);
|
||||
return 2;
|
||||
},
|
||||
);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.root).toBe(workspaceRoot);
|
||||
expect(payload.files).toEqual([
|
||||
expect.objectContaining({
|
||||
missing: false,
|
||||
path: "../shared/config.ts",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
missing: false,
|
||||
path: "src/readme.md",
|
||||
}),
|
||||
]);
|
||||
expect(
|
||||
payload.browser.entries.map((entry: Record<string, unknown>) => [
|
||||
entry.path,
|
||||
entry.kind,
|
||||
entry.sessionKind,
|
||||
]),
|
||||
).toEqual([
|
||||
["packages", "directory", "read"],
|
||||
["src", "directory", undefined],
|
||||
["ui", "directory", undefined],
|
||||
["package.json", "file", undefined],
|
||||
]);
|
||||
|
||||
const preview = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "src/readme.md",
|
||||
}),
|
||||
);
|
||||
expect(preview.file.content).toBe("# Nested read me\n");
|
||||
|
||||
const browserPreview = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "packages/app/src/readme.md",
|
||||
}),
|
||||
);
|
||||
expect(browserPreview.file.content).toBe("# Nested read me\n");
|
||||
|
||||
const parentRelativePreview = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "../shared/config.ts",
|
||||
}),
|
||||
);
|
||||
expect(parentRelativePreview.file.content).toBe("export const shared = true;\n");
|
||||
|
||||
const parentRelativeBrowserPreview = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "packages/shared/config.ts",
|
||||
}),
|
||||
);
|
||||
expect(parentRelativeBrowserPreview.file.content).toBe("export const shared = true;\n");
|
||||
});
|
||||
|
||||
it("falls back to the configured agent workspace for sessions without spawned metadata", async () => {
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:main:main",
|
||||
cfg: {},
|
||||
storePath: path.join(workspaceRoot, ".sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hoisted.resolveAgentWorkspaceDir).toHaveBeenCalledWith(expect.any(Object), "main");
|
||||
expect(payload.root).toBe(workspaceRoot);
|
||||
expect(payload.files).toEqual([
|
||||
expect.objectContaining({
|
||||
missing: false,
|
||||
path: "src/readme.md",
|
||||
}),
|
||||
]);
|
||||
expect(payload.browser).toBeDefined();
|
||||
});
|
||||
|
||||
it("uses the canonical session owner for configured workspace fallback", async () => {
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:aiden:main",
|
||||
cfg: {},
|
||||
storePath: path.join(workspaceRoot, ".sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "src/readme.md" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(hoisted.resolveAgentWorkspaceDir).toHaveBeenCalledWith(expect.any(Object), "aiden");
|
||||
expect(payload.root).toBe(workspaceRoot);
|
||||
expect(payload.files).toEqual([
|
||||
expect.objectContaining({
|
||||
missing: false,
|
||||
path: "src/readme.md",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("browses and searches workspace files without previewing browser-only files", async () => {
|
||||
const folderPayload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "ui",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(folderPayload.browser.parentPath).toBe("");
|
||||
expect(
|
||||
folderPayload.browser.entries.map((entry: Record<string, unknown>) => [
|
||||
entry.path,
|
||||
entry.kind,
|
||||
entry.sessionKind,
|
||||
]),
|
||||
).toEqual([
|
||||
["ui/chat.ts", "file", "modified"],
|
||||
["ui/vite.config.ts", "file", undefined],
|
||||
]);
|
||||
|
||||
const searchPayload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
search: "vite",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(searchPayload.browser.search).toBe("vite");
|
||||
expect(
|
||||
searchPayload.browser.entries.map((entry: Record<string, unknown>) => entry.path),
|
||||
).toEqual(["ui/vite.config.ts"]);
|
||||
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "ui/vite.config.ts",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(error.details).toMatchObject({
|
||||
path: "ui/vite.config.ts",
|
||||
type: "session_file_not_found",
|
||||
});
|
||||
});
|
||||
|
||||
it("truncates broad workspace searches by visited entries, not only by matches", async () => {
|
||||
for (let index = 0; index < 5_025; index += 1) {
|
||||
writeWorkspaceFile(workspaceRoot, `bulk-${String(index).padStart(4, "0")}.txt`, "");
|
||||
}
|
||||
writeWorkspaceFile(workspaceRoot, "zz-tail-needle.ts", "export const needle = true;\n");
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
search: "needle",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.browser).toMatchObject({
|
||||
search: "needle",
|
||||
truncated: true,
|
||||
});
|
||||
expect(payload.browser.entries).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not read absolute paths outside the configured workspace", async () => {
|
||||
const outsidePath = path.join(os.tmpdir(), `openclaw-outside-${Date.now()}.txt`);
|
||||
fs.writeFileSync(outsidePath, "outside\n", "utf8");
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:main:main",
|
||||
cfg: {},
|
||||
storePath: path.join(workspaceRoot, ".sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "missing-session.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: outsidePath }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: outsidePath,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(error.details).toMatchObject({
|
||||
path: outsidePath,
|
||||
type: "session_file_not_found",
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(outsidePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not follow workspace symlinks for file previews", async () => {
|
||||
const outsidePath = path.join(os.tmpdir(), `openclaw-linked-${Date.now()}.txt`);
|
||||
fs.writeFileSync(outsidePath, "linked outside\n", "utf8");
|
||||
fs.symlinkSync(outsidePath, path.join(workspaceRoot, "linked.txt"));
|
||||
|
||||
try {
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "linked.txt",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(error.details).toMatchObject({
|
||||
path: "linked.txt",
|
||||
type: "session_file_not_found",
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(outsidePath, { force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("does not follow symlinked parent directories for file previews", async () => {
|
||||
const outsideDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-linked-parent-"));
|
||||
writeWorkspaceFile(outsideDir, "secret.txt", "linked parent outside\n");
|
||||
fs.symlinkSync(outsideDir, path.join(workspaceRoot, "linked-dir"), "dir");
|
||||
|
||||
try {
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "linked-dir/secret.txt",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(error.details).toMatchObject({
|
||||
path: "linked-dir/secret.txt",
|
||||
type: "session_file_not_found",
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(outsideDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("returns integer file timestamps for protocol responses", async () => {
|
||||
const datedPath = path.join(workspaceRoot, "dated.txt");
|
||||
writeWorkspaceFile(workspaceRoot, "dated.txt", "dated\n");
|
||||
fs.utimesSync(datedPath, 1_700_000_000.123, 1_700_000_000.123);
|
||||
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
const entry = payload.browser.entries.find(
|
||||
(browserEntry: Record<string, unknown>) => browserEntry.path === "dated.txt",
|
||||
);
|
||||
|
||||
expect(Number.isInteger(entry.updatedAtMs)).toBe(true);
|
||||
});
|
||||
|
||||
it("does not browse paths outside the session workspace root", async () => {
|
||||
const payload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "../",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(payload.root).toBe(workspaceRoot);
|
||||
expect(payload.browser).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not derive a workspace root from transcript cwd", async () => {
|
||||
const sessionsDir = path.join(workspaceRoot, "custom-sessions");
|
||||
const transcriptCwd = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-transcript-cwd-"));
|
||||
writeWorkspaceFile(transcriptCwd, "secret.txt", "transcript cwd secret\n");
|
||||
fs.mkdirSync(sessionsDir, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(sessionsDir, "sess-main.jsonl"),
|
||||
`${JSON.stringify({ cwd: transcriptCwd })}\n`,
|
||||
"utf8",
|
||||
);
|
||||
hoisted.loadSessionEntry.mockReturnValue({
|
||||
canonicalKey: "agent:main:main",
|
||||
cfg: {},
|
||||
storePath: path.join(sessionsDir, "sessions.json"),
|
||||
entry: {
|
||||
sessionId: "sess-main",
|
||||
sessionFile: "sess-main.jsonl",
|
||||
},
|
||||
});
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "secret.txt" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
try {
|
||||
const listPayload = expectOkPayload(
|
||||
await invokeSessionFilesHandler("sessions.files.list", {
|
||||
sessionKey: "agent:main:main",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(listPayload.root).toBe(workspaceRoot);
|
||||
expect(listPayload.browser).toBeDefined();
|
||||
expect(listPayload.files).toMatchObject([
|
||||
{
|
||||
missing: true,
|
||||
path: "secret.txt",
|
||||
},
|
||||
]);
|
||||
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "secret.txt",
|
||||
}),
|
||||
);
|
||||
expect(error.details).toMatchObject({
|
||||
path: "secret.txt",
|
||||
type: "session_file_not_found",
|
||||
});
|
||||
} finally {
|
||||
fs.rmSync(transcriptCwd, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("reports oversized existing files without marking them missing", async () => {
|
||||
writeWorkspaceFile(workspaceRoot, "large.log", "x".repeat(260 * 1024));
|
||||
hoisted.visitSessionMessagesAsync.mockImplementation(
|
||||
async (_sessionId, _storePath, _sessionFile, visit) => {
|
||||
visit(assistantToolCall("read", { path: "large.log" }), 1);
|
||||
return 1;
|
||||
},
|
||||
);
|
||||
|
||||
const error = expectError(
|
||||
await invokeSessionFilesHandler("sessions.files.get", {
|
||||
sessionKey: "agent:main:main",
|
||||
path: "large.log",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(error.details).toMatchObject({
|
||||
maxPreviewBytes: 256 * 1024,
|
||||
path: "large.log",
|
||||
size: 260 * 1024,
|
||||
type: "session_file_too_large",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,737 @@
|
||||
// Gateway methods expose files referenced by one session transcript.
|
||||
import path from "node:path";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
ErrorCodes,
|
||||
errorShape,
|
||||
type SessionFileBrowserEntry,
|
||||
type SessionFileBrowserResult,
|
||||
type SessionFileEntry,
|
||||
type SessionFileRelevance,
|
||||
type SessionsFilesGetParams,
|
||||
validateSessionsFilesGetParams,
|
||||
validateSessionsFilesListParams,
|
||||
} from "../../../packages/gateway-protocol/src/index.js";
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../agents/agent-scope.js";
|
||||
import { root as fsSafeRoot, FsSafeError, type ReadResult } from "../../infra/fs-safe.js";
|
||||
import { normalizeAgentId, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { loadSessionEntry, visitSessionMessagesAsync } from "../session-utils.js";
|
||||
import type { GatewayRequestHandlers, RespondFn } from "./types.js";
|
||||
import { assertValidParams } from "./validation.js";
|
||||
|
||||
type FileKind = "modified" | "read";
|
||||
|
||||
type TouchedFile = {
|
||||
path: string;
|
||||
kind: FileKind;
|
||||
};
|
||||
|
||||
type WorkspaceRoot = Awaited<ReturnType<typeof fsSafeRoot>>;
|
||||
type WorkspacePathStat = Awaited<ReturnType<WorkspaceRoot["stat"]>>;
|
||||
type WorkspaceDirEntry = WorkspacePathStat & { name: string };
|
||||
type LoadedSessionFiles = {
|
||||
root?: string;
|
||||
fileRoot?: string;
|
||||
files: TouchedFile[];
|
||||
};
|
||||
|
||||
const MAX_PREVIEW_BYTES = 256 * 1024;
|
||||
const MAX_BROWSER_ENTRIES = 250;
|
||||
const MAX_SEARCH_ENTRIES = 500;
|
||||
const MAX_SEARCH_VISITED_ENTRIES = 5_000;
|
||||
const SEARCH_SKIP_DIRS = new Set([
|
||||
".git",
|
||||
".hg",
|
||||
".next",
|
||||
".turbo",
|
||||
".yarn",
|
||||
"coverage",
|
||||
"dist",
|
||||
"node_modules",
|
||||
]);
|
||||
|
||||
function sessionFilesError(type: string, message: string, details?: Record<string, unknown>) {
|
||||
return errorShape(ErrorCodes.INVALID_REQUEST, message, {
|
||||
details: {
|
||||
type,
|
||||
...details,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : undefined;
|
||||
}
|
||||
|
||||
function normalizePathValue(value: unknown): string | undefined {
|
||||
if (typeof value !== "string") {
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
function readPathArg(args: Record<string, unknown>): string | undefined {
|
||||
return (
|
||||
normalizePathValue(args.path) ??
|
||||
normalizePathValue(args.file_path) ??
|
||||
normalizePathValue(args.filePath) ??
|
||||
normalizePathValue(args.file)
|
||||
);
|
||||
}
|
||||
|
||||
function addTouchedFile(
|
||||
files: Map<string, TouchedFile>,
|
||||
filePath: string | undefined,
|
||||
kind: FileKind,
|
||||
) {
|
||||
if (!filePath) {
|
||||
return;
|
||||
}
|
||||
const existing = files.get(filePath);
|
||||
if (existing?.kind === "modified" || (existing && kind === "read")) {
|
||||
return;
|
||||
}
|
||||
files.set(filePath, { path: filePath, kind });
|
||||
}
|
||||
|
||||
function addRawPatchFiles(files: Map<string, TouchedFile>, input: unknown) {
|
||||
if (typeof input !== "string") {
|
||||
return;
|
||||
}
|
||||
const fileLinePattern = /^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm;
|
||||
for (const match of input.matchAll(fileLinePattern)) {
|
||||
addTouchedFile(files, match[1]?.trim(), "modified");
|
||||
}
|
||||
const moveLinePattern = /^\*\*\* Move to: (.+)$/gm;
|
||||
for (const match of input.matchAll(moveLinePattern)) {
|
||||
addTouchedFile(files, match[1]?.trim(), "modified");
|
||||
}
|
||||
}
|
||||
|
||||
function addStructuredPatchFiles(files: Map<string, TouchedFile>, changes: unknown) {
|
||||
if (!Array.isArray(changes)) {
|
||||
return;
|
||||
}
|
||||
for (const changeValue of changes) {
|
||||
const change = asRecord(changeValue);
|
||||
addTouchedFile(files, normalizePathValue(change?.path), "modified");
|
||||
const kind = asRecord(change?.kind);
|
||||
addTouchedFile(
|
||||
files,
|
||||
normalizePathValue(kind?.move_path) ?? normalizePathValue(kind?.movePath),
|
||||
"modified",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function addPatchFiles(files: Map<string, TouchedFile>, args: Record<string, unknown>) {
|
||||
addRawPatchFiles(files, args.input);
|
||||
addStructuredPatchFiles(files, args.changes);
|
||||
}
|
||||
|
||||
function isToolCallBlockType(value: unknown): boolean {
|
||||
if (typeof value !== "string") {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.toLowerCase().replace(/[_-]/g, "");
|
||||
return normalized === "toolcall" || normalized === "tooluse";
|
||||
}
|
||||
|
||||
function collectTouchedFilesFromMessage(message: unknown, files: Map<string, TouchedFile>) {
|
||||
const record = asRecord(message);
|
||||
if (record?.role !== "assistant" || !Array.isArray(record.content)) {
|
||||
return;
|
||||
}
|
||||
for (const blockValue of record.content) {
|
||||
const block = asRecord(blockValue);
|
||||
if (!block || !isToolCallBlockType(block.type)) {
|
||||
continue;
|
||||
}
|
||||
const toolName = normalizeOptionalString(block.name)?.toLowerCase();
|
||||
const args = asRecord(block.arguments) ?? asRecord(block.input) ?? asRecord(block.args);
|
||||
if (!toolName || !args) {
|
||||
continue;
|
||||
}
|
||||
if (toolName === "read") {
|
||||
addTouchedFile(files, readPathArg(args), "read");
|
||||
} else if (toolName === "write" || toolName === "edit") {
|
||||
addTouchedFile(files, readPathArg(args), "modified");
|
||||
} else if (toolName === "apply_patch") {
|
||||
addPatchFiles(files, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function toDisplayPath(root: string, resolved: string): string {
|
||||
const relative = path.relative(root, resolved);
|
||||
if (!relative) {
|
||||
return "";
|
||||
}
|
||||
return relative.split(path.sep).join("/");
|
||||
}
|
||||
|
||||
function normalizeRelativePath(value: string | undefined): string {
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
return value
|
||||
.replaceAll("\\", "/")
|
||||
.split("/")
|
||||
.filter((part) => part && part !== ".")
|
||||
.join("/");
|
||||
}
|
||||
|
||||
function resolveWorkspacePath(root: string | undefined, filePath: string): string | undefined {
|
||||
if (!root) {
|
||||
return undefined;
|
||||
}
|
||||
const resolved = path.isAbsolute(filePath)
|
||||
? path.resolve(filePath)
|
||||
: path.resolve(root, filePath);
|
||||
const relative = path.relative(root, resolved);
|
||||
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
||||
return undefined;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function isInsideRoot(root: string, candidate: string): boolean {
|
||||
const relative = path.relative(root, candidate);
|
||||
return !relative.startsWith("..") && !path.isAbsolute(relative);
|
||||
}
|
||||
|
||||
function resolveTouchedFilePath(params: {
|
||||
root: string | undefined;
|
||||
fileRoot: string | undefined;
|
||||
filePath: string;
|
||||
}): string | undefined {
|
||||
if (!params.root) {
|
||||
return undefined;
|
||||
}
|
||||
const base = params.fileRoot ?? params.root;
|
||||
const resolved = path.isAbsolute(params.filePath)
|
||||
? path.resolve(params.filePath)
|
||||
: path.resolve(base, params.filePath);
|
||||
if (!isInsideRoot(params.root, resolved)) {
|
||||
return undefined;
|
||||
}
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function resolveFileRoot(params: {
|
||||
root: string | undefined;
|
||||
spawnedCwd: string | undefined;
|
||||
}): string | undefined {
|
||||
if (!params.root) {
|
||||
return undefined;
|
||||
}
|
||||
if (!params.spawnedCwd) {
|
||||
return params.root;
|
||||
}
|
||||
const resolvedCwd = path.resolve(params.spawnedCwd);
|
||||
const resolvedRoot = path.resolve(params.root);
|
||||
return isInsideRoot(resolvedRoot, resolvedCwd) ? params.spawnedCwd : params.root;
|
||||
}
|
||||
|
||||
async function openSessionWorkspaceRoot(rootDir: string): Promise<WorkspaceRoot | undefined> {
|
||||
try {
|
||||
return await fsSafeRoot(rootDir, {
|
||||
hardlinks: "reject",
|
||||
maxBytes: MAX_PREVIEW_BYTES,
|
||||
nonBlockingRead: true,
|
||||
symlinks: "reject",
|
||||
});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function statWorkspacePath(
|
||||
rootDir: string,
|
||||
browserPath: string,
|
||||
): Promise<WorkspacePathStat | undefined> {
|
||||
const workspaceRoot = await openSessionWorkspaceRoot(rootDir);
|
||||
if (!workspaceRoot) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await workspaceRoot.stat(browserPath || ".");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function listWorkspacePath(
|
||||
rootDir: string,
|
||||
browserPath: string,
|
||||
): Promise<WorkspaceDirEntry[] | undefined> {
|
||||
const workspaceRoot = await openSessionWorkspaceRoot(rootDir);
|
||||
if (!workspaceRoot) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await workspaceRoot.list(browserPath || ".", { withFileTypes: true });
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function readWorkspaceFile(
|
||||
rootDir: string,
|
||||
browserPath: string,
|
||||
): Promise<ReadResult | undefined | "too-large"> {
|
||||
const workspaceRoot = await openSessionWorkspaceRoot(rootDir);
|
||||
if (!workspaceRoot) {
|
||||
return undefined;
|
||||
}
|
||||
try {
|
||||
return await workspaceRoot.read(browserPath, {
|
||||
hardlinks: "reject",
|
||||
maxBytes: MAX_PREVIEW_BYTES,
|
||||
nonBlockingRead: true,
|
||||
symlinks: "reject",
|
||||
});
|
||||
} catch (err) {
|
||||
if (err instanceof FsSafeError && err.code === "too-large") {
|
||||
return "too-large";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function relevanceForKind(kind: FileKind): SessionFileRelevance {
|
||||
return kind;
|
||||
}
|
||||
|
||||
function mergeRelevance(
|
||||
current: SessionFileRelevance | undefined,
|
||||
next: SessionFileRelevance | undefined,
|
||||
): SessionFileRelevance | undefined {
|
||||
if (!current) {
|
||||
return next;
|
||||
}
|
||||
if (!next || current === next) {
|
||||
return current;
|
||||
}
|
||||
return "mixed";
|
||||
}
|
||||
|
||||
function buildSessionRelevanceMap(
|
||||
files: readonly TouchedFile[],
|
||||
root: string | undefined,
|
||||
fileRoot: string | undefined,
|
||||
): Map<string, SessionFileRelevance> {
|
||||
const relevance = new Map<string, SessionFileRelevance>();
|
||||
if (!root) {
|
||||
for (const file of files) {
|
||||
relevance.set(normalizeRelativePath(file.path), relevanceForKind(file.kind));
|
||||
}
|
||||
return relevance;
|
||||
}
|
||||
for (const file of files) {
|
||||
const resolved = resolveTouchedFilePath({ root, fileRoot, filePath: file.path });
|
||||
if (!resolved) {
|
||||
continue;
|
||||
}
|
||||
relevance.set(toDisplayPath(root, resolved), relevanceForKind(file.kind));
|
||||
}
|
||||
return relevance;
|
||||
}
|
||||
|
||||
function relevanceForBrowserPath(
|
||||
browserPath: string,
|
||||
kind: "file" | "directory",
|
||||
relevance: ReadonlyMap<string, SessionFileRelevance>,
|
||||
): SessionFileRelevance | undefined {
|
||||
if (kind === "file") {
|
||||
return relevance.get(browserPath);
|
||||
}
|
||||
const prefix = browserPath ? `${browserPath}/` : "";
|
||||
let aggregate: SessionFileRelevance | undefined;
|
||||
for (const [filePath, sessionKind] of relevance) {
|
||||
if (filePath.startsWith(prefix) && filePath !== browserPath) {
|
||||
aggregate = mergeRelevance(aggregate, sessionKind);
|
||||
}
|
||||
}
|
||||
return aggregate;
|
||||
}
|
||||
|
||||
function displayNameForPath(filePath: string): string {
|
||||
const base = path.basename(filePath);
|
||||
return base || filePath;
|
||||
}
|
||||
|
||||
function toUpdatedAtMs(mtimeMs: number): number {
|
||||
return Math.floor(mtimeMs);
|
||||
}
|
||||
|
||||
function workspaceStatKind(stat: WorkspacePathStat): "file" | "directory" | "symlink" | undefined {
|
||||
const kind = (stat as { kind?: unknown }).kind;
|
||||
if (kind === "file" || kind === "directory" || kind === "symlink") {
|
||||
return kind;
|
||||
}
|
||||
const nodeStat = stat as {
|
||||
isDirectory?: boolean | (() => boolean);
|
||||
isFile?: boolean | (() => boolean);
|
||||
isSymbolicLink?: boolean | (() => boolean);
|
||||
};
|
||||
const isFile = typeof nodeStat.isFile === "function" ? nodeStat.isFile() : nodeStat.isFile;
|
||||
if (isFile) {
|
||||
return "file";
|
||||
}
|
||||
const isDirectory =
|
||||
typeof nodeStat.isDirectory === "function" ? nodeStat.isDirectory() : nodeStat.isDirectory;
|
||||
if (isDirectory) {
|
||||
return "directory";
|
||||
}
|
||||
const isSymbolicLink =
|
||||
typeof nodeStat.isSymbolicLink === "function"
|
||||
? nodeStat.isSymbolicLink()
|
||||
: nodeStat.isSymbolicLink;
|
||||
return isSymbolicLink ? "symlink" : undefined;
|
||||
}
|
||||
|
||||
async function toSessionFileEntry(
|
||||
touched: TouchedFile,
|
||||
root: string | undefined,
|
||||
fileRoot: string | undefined,
|
||||
opts: { includeContent?: boolean } = {},
|
||||
): Promise<SessionFileEntry> {
|
||||
const resolved = resolveTouchedFilePath({ root, fileRoot, filePath: touched.path });
|
||||
const base = {
|
||||
path: touched.path,
|
||||
name: displayNameForPath(touched.path),
|
||||
kind: touched.kind,
|
||||
} satisfies Pick<SessionFileEntry, "path" | "name" | "kind">;
|
||||
if (!resolved) {
|
||||
return { ...base, missing: true };
|
||||
}
|
||||
const browserPath = toDisplayPath(root!, resolved);
|
||||
const stat = await statWorkspacePath(root!, browserPath);
|
||||
if (!stat || workspaceStatKind(stat) !== "file") {
|
||||
return { ...base, missing: true };
|
||||
}
|
||||
const entry: SessionFileEntry = {
|
||||
...base,
|
||||
missing: false,
|
||||
size: stat.size,
|
||||
updatedAtMs: toUpdatedAtMs(stat.mtimeMs),
|
||||
};
|
||||
if (opts.includeContent && stat.size <= MAX_PREVIEW_BYTES) {
|
||||
const read = await readWorkspaceFile(root!, browserPath);
|
||||
if (!read) {
|
||||
return { ...base, missing: true };
|
||||
}
|
||||
if (read !== "too-large") {
|
||||
entry.size = read.stat.size;
|
||||
entry.updatedAtMs = toUpdatedAtMs(read.stat.mtimeMs);
|
||||
entry.content = read.buffer.toString("utf8");
|
||||
}
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
async function toBrowserEntry(
|
||||
browserPath: string,
|
||||
dirent: WorkspaceDirEntry,
|
||||
relevance: ReadonlyMap<string, SessionFileRelevance>,
|
||||
): Promise<SessionFileBrowserEntry | undefined> {
|
||||
const statKind = workspaceStatKind(dirent);
|
||||
const kind = statKind === "directory" ? "directory" : statKind === "file" ? "file" : null;
|
||||
if (!kind) {
|
||||
return undefined;
|
||||
}
|
||||
const sessionKind = relevanceForBrowserPath(browserPath, kind, relevance);
|
||||
return {
|
||||
path: browserPath,
|
||||
name: dirent.name,
|
||||
kind,
|
||||
...(kind === "file" ? { size: dirent.size } : {}),
|
||||
updatedAtMs: toUpdatedAtMs(dirent.mtimeMs),
|
||||
...(sessionKind ? { sessionKind } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function sortBrowserEntries(
|
||||
entries: readonly SessionFileBrowserEntry[],
|
||||
): SessionFileBrowserEntry[] {
|
||||
return entries.toSorted((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
return a.kind === "directory" ? -1 : 1;
|
||||
}
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}
|
||||
|
||||
function sortDirents<T extends { name: string }>(dirents: readonly T[]): T[] {
|
||||
return dirents.toSorted((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
|
||||
function matchesSearch(entryPath: string, name: string, query: string): boolean {
|
||||
const normalizedQuery = query.toLowerCase();
|
||||
return (
|
||||
name.toLowerCase().includes(normalizedQuery) ||
|
||||
entryPath.toLowerCase().includes(normalizedQuery)
|
||||
);
|
||||
}
|
||||
|
||||
async function searchBrowserEntries(params: {
|
||||
root: string;
|
||||
query: string;
|
||||
relevance: ReadonlyMap<string, SessionFileRelevance>;
|
||||
}): Promise<{ entries: SessionFileBrowserEntry[]; truncated?: boolean }> {
|
||||
const entries: SessionFileBrowserEntry[] = [];
|
||||
let visitedEntries = 0;
|
||||
let truncated = false;
|
||||
const shouldStop = (): boolean => {
|
||||
if (entries.length >= MAX_SEARCH_ENTRIES || visitedEntries >= MAX_SEARCH_VISITED_ENTRIES) {
|
||||
truncated = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
const visit = async (dir: string): Promise<void> => {
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
const dirents = await listWorkspacePath(params.root, dir);
|
||||
if (!dirents) {
|
||||
return;
|
||||
}
|
||||
for (const dirent of sortDirents(dirents)) {
|
||||
if (shouldStop()) {
|
||||
return;
|
||||
}
|
||||
visitedEntries += 1;
|
||||
const browserPath = dir ? `${dir}/${dirent.name}` : dirent.name;
|
||||
if (matchesSearch(browserPath, dirent.name, params.query)) {
|
||||
const entry = await toBrowserEntry(browserPath, dirent, params.relevance);
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
}
|
||||
if (workspaceStatKind(dirent) === "directory" && !SEARCH_SKIP_DIRS.has(dirent.name)) {
|
||||
await visit(browserPath);
|
||||
}
|
||||
}
|
||||
};
|
||||
await visit("");
|
||||
return { entries: sortBrowserEntries(entries), ...(truncated ? { truncated } : {}) };
|
||||
}
|
||||
|
||||
async function buildBrowserResult(params: {
|
||||
root: string | undefined;
|
||||
fileRoot: string | undefined;
|
||||
path?: string;
|
||||
search?: string;
|
||||
files: readonly TouchedFile[];
|
||||
}): Promise<SessionFileBrowserResult | undefined> {
|
||||
if (!params.root) {
|
||||
return undefined;
|
||||
}
|
||||
const search = normalizePathValue(params.search);
|
||||
const relevance = buildSessionRelevanceMap(params.files, params.root, params.fileRoot);
|
||||
if (search) {
|
||||
const result = await searchBrowserEntries({
|
||||
root: params.root,
|
||||
query: search,
|
||||
relevance,
|
||||
});
|
||||
return {
|
||||
path: "",
|
||||
search,
|
||||
entries: result.entries,
|
||||
...(result.truncated ? { truncated: result.truncated } : {}),
|
||||
};
|
||||
}
|
||||
const browserPath = normalizeRelativePath(params.path);
|
||||
const resolved = resolveWorkspacePath(params.root, browserPath);
|
||||
if (!resolved) {
|
||||
return undefined;
|
||||
}
|
||||
const stat = await statWorkspacePath(params.root, browserPath);
|
||||
if (!stat || workspaceStatKind(stat) !== "directory") {
|
||||
return undefined;
|
||||
}
|
||||
const dirents = await listWorkspacePath(params.root, browserPath);
|
||||
if (!dirents) {
|
||||
return undefined;
|
||||
}
|
||||
const entries = (
|
||||
await Promise.all(
|
||||
sortDirents(dirents)
|
||||
.slice(0, MAX_BROWSER_ENTRIES + 1)
|
||||
.map((dirent) => {
|
||||
const entryPath = browserPath ? `${browserPath}/${dirent.name}` : dirent.name;
|
||||
return toBrowserEntry(entryPath, dirent, relevance);
|
||||
}),
|
||||
)
|
||||
).filter((entry): entry is SessionFileBrowserEntry => Boolean(entry));
|
||||
const parent = path.dirname(browserPath);
|
||||
return {
|
||||
path: browserPath,
|
||||
...(browserPath ? { parentPath: parent === "." ? "" : parent } : {}),
|
||||
entries: sortBrowserEntries(entries.slice(0, MAX_BROWSER_ENTRIES)),
|
||||
...(entries.length > MAX_BROWSER_ENTRIES ? { truncated: true } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function loadSessionFiles(params: {
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
}): Promise<LoadedSessionFiles> {
|
||||
const { cfg, storePath, entry, canonicalKey } = loadSessionEntry(params.sessionKey, {
|
||||
agentId: params.agentId,
|
||||
});
|
||||
if (!entry?.sessionId || !storePath) {
|
||||
return { files: [] };
|
||||
}
|
||||
const agentId = normalizeAgentId(
|
||||
parseAgentSessionKey(canonicalKey)?.agentId ??
|
||||
params.agentId ??
|
||||
parseAgentSessionKey(params.sessionKey)?.agentId ??
|
||||
resolveDefaultAgentId(cfg),
|
||||
);
|
||||
const spawnedCwd = normalizePathValue(entry.spawnedCwd);
|
||||
const root =
|
||||
normalizePathValue(entry.spawnedWorkspaceDir) ??
|
||||
spawnedCwd ??
|
||||
normalizePathValue(resolveAgentWorkspaceDir(cfg, agentId));
|
||||
const fileRoot = resolveFileRoot({ root, spawnedCwd });
|
||||
const files = new Map<string, TouchedFile>();
|
||||
await visitSessionMessagesAsync(
|
||||
entry.sessionId,
|
||||
storePath,
|
||||
entry.sessionFile,
|
||||
(message) => collectTouchedFilesFromMessage(message, files),
|
||||
{
|
||||
mode: "full",
|
||||
reason: "session files transcript scan",
|
||||
cache: "reuse",
|
||||
},
|
||||
);
|
||||
return {
|
||||
root,
|
||||
fileRoot,
|
||||
files: [...files.values()].toSorted((a, b) => {
|
||||
if (a.kind !== b.kind) {
|
||||
return a.kind === "modified" ? -1 : 1;
|
||||
}
|
||||
return a.path.localeCompare(b.path);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
async function buildListResult(params: {
|
||||
sessionKey: string;
|
||||
agentId?: string;
|
||||
path?: string;
|
||||
search?: string;
|
||||
}): Promise<{ root?: string; files: SessionFileEntry[]; browser?: SessionFileBrowserResult }> {
|
||||
const loaded = await loadSessionFiles(params);
|
||||
const files = await Promise.all(
|
||||
loaded.files.map((file) => toSessionFileEntry(file, loaded.root, loaded.fileRoot)),
|
||||
);
|
||||
const browser = await buildBrowserResult({
|
||||
root: loaded.root,
|
||||
fileRoot: loaded.fileRoot,
|
||||
path: params.path,
|
||||
search: params.search,
|
||||
files: loaded.files,
|
||||
});
|
||||
return {
|
||||
...(loaded.root ? { root: loaded.root } : {}),
|
||||
files,
|
||||
...(browser ? { browser } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function findSessionFile(
|
||||
params: SessionsFilesGetParams,
|
||||
): Promise<{ root?: string; file?: SessionFileEntry }> {
|
||||
const loaded = await loadSessionFiles(params);
|
||||
const exactTouched = loaded.files.find((file) => file.path === params.path);
|
||||
if (exactTouched) {
|
||||
return {
|
||||
...(loaded.root ? { root: loaded.root } : {}),
|
||||
file: await toSessionFileEntry(exactTouched, loaded.root, loaded.fileRoot, {
|
||||
includeContent: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
const resolved = resolveWorkspacePath(loaded.root, params.path);
|
||||
if (!resolved || !loaded.root) {
|
||||
return loaded.root ? { root: loaded.root } : {};
|
||||
}
|
||||
const relevance = buildSessionRelevanceMap(loaded.files, loaded.root, loaded.fileRoot);
|
||||
const browserPath = toDisplayPath(loaded.root, resolved);
|
||||
const sessionKind = relevance.get(browserPath);
|
||||
if (!sessionKind) {
|
||||
return loaded.root ? { root: loaded.root } : {};
|
||||
}
|
||||
const touched: TouchedFile = {
|
||||
path: browserPath,
|
||||
kind: sessionKind === "modified" ? "modified" : "read",
|
||||
};
|
||||
return {
|
||||
...(loaded.root ? { root: loaded.root } : {}),
|
||||
file: await toSessionFileEntry(touched, loaded.root, loaded.root, {
|
||||
includeContent: true,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function respondSessionFileNotFound(respond: RespondFn, filePath: string) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
sessionFilesError("session_file_not_found", "session file not found", { path: filePath }),
|
||||
);
|
||||
}
|
||||
|
||||
function respondSessionFileTooLarge(respond: RespondFn, file: SessionFileEntry, filePath: string) {
|
||||
respond(
|
||||
false,
|
||||
undefined,
|
||||
sessionFilesError("session_file_too_large", "session file is too large to preview", {
|
||||
maxPreviewBytes: MAX_PREVIEW_BYTES,
|
||||
path: file.path || filePath,
|
||||
size: file.size,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/** Gateway handlers for files referenced by session transcripts. */
|
||||
export const sessionsFilesHandlers: GatewayRequestHandlers = {
|
||||
"sessions.files.list": async ({ params, respond }) => {
|
||||
if (
|
||||
!assertValidParams(params, validateSessionsFilesListParams, "sessions.files.list", respond)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const result = await buildListResult(params);
|
||||
respond(true, {
|
||||
sessionKey: params.sessionKey,
|
||||
...result,
|
||||
});
|
||||
},
|
||||
"sessions.files.get": async ({ params, respond }) => {
|
||||
if (!assertValidParams(params, validateSessionsFilesGetParams, "sessions.files.get", respond)) {
|
||||
return;
|
||||
}
|
||||
const result = await findSessionFile(params);
|
||||
if (typeof result.file?.content !== "string") {
|
||||
if (result.file && !result.file.missing) {
|
||||
respondSessionFileTooLarge(respond, result.file, params.path);
|
||||
return;
|
||||
}
|
||||
respondSessionFileNotFound(respond, params.path);
|
||||
return;
|
||||
}
|
||||
respond(true, {
|
||||
sessionKey: params.sessionKey,
|
||||
...result,
|
||||
});
|
||||
},
|
||||
};
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.191Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.297Z",
|
||||
"locale": "ar",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.731Z",
|
||||
"generatedAt": "2026-06-14T04:03:17.952Z",
|
||||
"locale": "de",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.822Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.021Z",
|
||||
"locale": "es",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:12.036Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.916Z",
|
||||
"locale": "fa",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.096Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.227Z",
|
||||
"locale": "fr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.562Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.574Z",
|
||||
"locale": "id",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.279Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.366Z",
|
||||
"locale": "it",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.912Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.090Z",
|
||||
"locale": "ja-JP",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.005Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.160Z",
|
||||
"locale": "ko",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.940Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.847Z",
|
||||
"locale": "nl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.653Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.642Z",
|
||||
"locale": "pl",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.631Z",
|
||||
"generatedAt": "2026-06-14T04:03:17.881Z",
|
||||
"locale": "pt-BR",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+1
-1
@@ -4027,7 +4027,7 @@
|
||||
"text": "Sanitized rich-text preview for quick reading."
|
||||
},
|
||||
{
|
||||
"count": 3,
|
||||
"count": 4,
|
||||
"kind": "html-text",
|
||||
"name": "text",
|
||||
"path": "ui/src/ui/views/markdown-sidebar.ts",
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.747Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.710Z",
|
||||
"locale": "th",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.372Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.435Z",
|
||||
"locale": "tr",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.468Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.507Z",
|
||||
"locale": "uk",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:11.843Z",
|
||||
"generatedAt": "2026-06-14T04:03:18.780Z",
|
||||
"locale": "vi",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.441Z",
|
||||
"generatedAt": "2026-06-14T04:03:17.740Z",
|
||||
"locale": "zh-CN",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+25
-15
@@ -1,27 +1,37 @@
|
||||
{
|
||||
"fallbackKeys": [
|
||||
"chat.workspaceFiles.actions",
|
||||
"chat.workspaceFiles.artifactCount",
|
||||
"chat.workspaceFiles.artifacts",
|
||||
"chat.workspaceFiles.browser",
|
||||
"chat.workspaceFiles.browserCount",
|
||||
"chat.workspaceFiles.changed",
|
||||
"chat.workspaceFiles.changedCount",
|
||||
"chat.workspaceFiles.collapse",
|
||||
"chat.workspaceFiles.copyPath",
|
||||
"chat.workspaceFiles.empty",
|
||||
"chat.workspaceFiles.expand",
|
||||
"chat.workspaceFiles.files",
|
||||
"chat.workspaceFiles.label",
|
||||
"chat.workspaceFiles.loading",
|
||||
"chat.workspaceFiles.missing",
|
||||
"chat.workspaceFiles.noBrowserFiles",
|
||||
"chat.workspaceFiles.noSearchResults",
|
||||
"chat.workspaceFiles.parentFolder",
|
||||
"chat.workspaceFiles.path",
|
||||
"chat.workspaceFiles.preview",
|
||||
"chat.workspaceFiles.read",
|
||||
"chat.workspaceFiles.readCount",
|
||||
"chat.workspaceFiles.refresh",
|
||||
"chat.workspaceFiles.root",
|
||||
"chat.workspaceFiles.search",
|
||||
"chat.workspaceFiles.searchResults",
|
||||
"chat.workspaceFiles.session",
|
||||
"chat.workspaceFiles.summary",
|
||||
"chat.workspaceFiles.truncated",
|
||||
"chat.workspaceFiles.workspace",
|
||||
"cron.jobDetail.command",
|
||||
"cron.jobDetail.cwd",
|
||||
"logsView.autoFollow",
|
||||
"logsView.empty",
|
||||
"logsView.exportButton",
|
||||
"logsView.exportLabels.filtered",
|
||||
"logsView.exportLabels.visible",
|
||||
"logsView.file",
|
||||
"logsView.filter",
|
||||
"logsView.searchPlaceholder",
|
||||
"logsView.subtitle",
|
||||
"logsView.title",
|
||||
"logsView.truncated",
|
||||
"skillWorkshop.header.useCurrentChat",
|
||||
"skillWorkshop.header.useCurrentChatAria",
|
||||
"skillWorkshop.header.useCurrentChatTooltip",
|
||||
@@ -54,12 +64,12 @@
|
||||
"workboard.unknownStatus",
|
||||
"workboard.viewDetails"
|
||||
],
|
||||
"generatedAt": "2026-06-14T00:56:10.535Z",
|
||||
"generatedAt": "2026-06-14T04:03:17.811Z",
|
||||
"locale": "zh-TW",
|
||||
"model": "claude-opus-4-8",
|
||||
"provider": "anthropic",
|
||||
"sourceHash": "d0b2c5f7e87d471ad2373c4ea8875db08c9bab459b4c4c5573e5eff03ee5be12",
|
||||
"totalKeys": 1356,
|
||||
"translatedKeys": 1303,
|
||||
"sourceHash": "cea79b090b14fca4ad1e3e07dc5dcd7f964b1f199fd028938e41054dc5167457",
|
||||
"totalKeys": 1377,
|
||||
"translatedKeys": 1314,
|
||||
"workflow": 1
|
||||
}
|
||||
|
||||
Generated
+21
@@ -1354,7 +1354,28 @@ export const ar: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1379,7 +1379,28 @@ export const de: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
@@ -1352,15 +1352,36 @@ export const en: TranslationMap = {
|
||||
toolError: "Tool error",
|
||||
},
|
||||
workspaceFiles: {
|
||||
label: "Workspace files",
|
||||
expand: "Expand workspace files",
|
||||
collapse: "Collapse workspace files",
|
||||
workspace: "Workspace",
|
||||
files: "Files",
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
label: "Session workspace",
|
||||
expand: "Expand session workspace",
|
||||
collapse: "Collapse session workspace",
|
||||
workspace: "Session",
|
||||
files: "Workspace",
|
||||
refresh: "Refresh session workspace",
|
||||
loading: "Loading session workspace…",
|
||||
empty: "No files touched in this session yet",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1376,7 +1376,28 @@ export const es: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1371,7 +1371,28 @@ export const fa: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1383,7 +1383,28 @@ export const fr: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1369,7 +1369,28 @@ export const id: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1376,7 +1376,28 @@ export const it: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1373,7 +1373,28 @@ export const ja_JP: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1362,7 +1362,28 @@ export const ko: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1374,7 +1374,28 @@ export const nl: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1374,7 +1374,28 @@ export const pl: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1370,7 +1370,28 @@ export const pt_BR: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1339,7 +1339,28 @@ export const th: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1376,7 +1376,28 @@ export const tr: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1373,7 +1373,28 @@ export const uk: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1362,7 +1362,28 @@ export const vi: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1334,7 +1334,28 @@ export const zh_CN: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
Generated
+21
@@ -1336,7 +1336,28 @@ export const zh_TW: TranslationMap = {
|
||||
refresh: "Refresh files",
|
||||
loading: "Loading files…",
|
||||
empty: "No workspace files",
|
||||
changed: "Changed",
|
||||
read: "Read",
|
||||
artifacts: "Artifacts",
|
||||
browser: "Project files",
|
||||
path: "Workspace path",
|
||||
root: "Root",
|
||||
search: "Search files",
|
||||
searchResults: "Search results",
|
||||
parentFolder: "Parent folder",
|
||||
noBrowserFiles: "No files in this folder.",
|
||||
noSearchResults: "No matching files.",
|
||||
truncated: "Showing the first matching files. Refine the search to narrow results.",
|
||||
session: "Session",
|
||||
missing: "Missing",
|
||||
summary: "Session workspace summary",
|
||||
changedCount: "{count} changed",
|
||||
readCount: "{count} read",
|
||||
artifactCount: "{count} artifacts",
|
||||
browserCount: "{count} shown",
|
||||
actions: "Workspace file actions",
|
||||
preview: "Preview",
|
||||
copyPath: "Copy path",
|
||||
},
|
||||
},
|
||||
languages: {
|
||||
|
||||
+213
-11
@@ -37,7 +37,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
animation: slide-in 200ms ease-out;
|
||||
animation: chat-sidebar-fade-in 160ms ease-out;
|
||||
}
|
||||
|
||||
.chat-workspace-rail {
|
||||
@@ -159,39 +159,107 @@
|
||||
color: var(--danger, #ef4444);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__scroll {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__list--browser {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__section-title {
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
line-height: 1.1;
|
||||
padding: 0 12px 6px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 0 12px 8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__summary span {
|
||||
padding: 2px 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 70%, transparent);
|
||||
border-radius: var(--radius-full);
|
||||
background: color-mix(in srgb, var(--bg-elevated) 62%, transparent);
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-height: 38px;
|
||||
padding: 7px 8px;
|
||||
padding: 0 6px 0 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file:hover,
|
||||
.chat-workspace-rail__file:focus-visible,
|
||||
.chat-workspace-rail__file:focus-within,
|
||||
.chat-workspace-rail__file--active {
|
||||
border-color: color-mix(in srgb, var(--border-strong) 62%, transparent);
|
||||
background: var(--bg-hover);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file-open {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
padding: 7px 8px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file-open:focus-visible {
|
||||
outline: 2px solid color-mix(in srgb, var(--accent, #7dd3fc) 65%, transparent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file--directory .chat-workspace-rail__file-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -227,14 +295,148 @@
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@keyframes slide-in {
|
||||
.chat-workspace-rail__row-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
opacity: 0;
|
||||
transition: opacity 120ms ease;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__file:hover .chat-workspace-rail__row-actions,
|
||||
.chat-workspace-rail__file:focus-within .chat-workspace-rail__row-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__row-action {
|
||||
display: inline-grid;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
place-items: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__row-action:hover,
|
||||
.chat-workspace-rail__row-action:focus-visible {
|
||||
border-color: color-mix(in srgb, var(--border-strong) 72%, transparent);
|
||||
background: color-mix(in srgb, var(--bg-elevated) 82%, transparent);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__row-action svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__browser {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__browser-tools {
|
||||
padding: 0 8px 8px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid color-mix(in srgb, var(--border) 78%, transparent);
|
||||
border-radius: var(--radius-md);
|
||||
background: color-mix(in srgb, var(--bg-elevated) 70%, transparent);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__search-icon {
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__search-icon svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1.5px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__search input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
font-size: var(--control-ui-text-sm);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__search input::placeholder {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__breadcrumbs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
padding: 0 12px 6px;
|
||||
color: var(--muted);
|
||||
font-size: var(--control-ui-text-xs);
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__crumb {
|
||||
min-width: 0;
|
||||
max-width: 88px;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__crumb:hover,
|
||||
.chat-workspace-rail__crumb:focus-visible {
|
||||
color: var(--text);
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.chat-workspace-rail__crumb-separator,
|
||||
.chat-workspace-rail__browser-caption {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.chat-workspace-rail__browser-caption {
|
||||
padding: 0 12px 6px;
|
||||
font-size: var(--control-ui-text-xs);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@keyframes chat-sidebar-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -433,6 +433,23 @@ function installControlUiMockGateway(input: {
|
||||
};
|
||||
case "agents.files.get":
|
||||
return null;
|
||||
case "sessions.files.list":
|
||||
return {
|
||||
browser: {
|
||||
entries: [],
|
||||
path: "",
|
||||
},
|
||||
files: [],
|
||||
root: "",
|
||||
sessionKey:
|
||||
isRecord(params) && typeof params.sessionKey === "string" ? params.sessionKey : "main",
|
||||
};
|
||||
case "sessions.files.get":
|
||||
return null;
|
||||
case "artifacts.list":
|
||||
return { artifacts: [] };
|
||||
case "artifacts.download":
|
||||
return null;
|
||||
case "chat.history":
|
||||
return {
|
||||
messages: scenario.historyMessages,
|
||||
|
||||
+271
-44
@@ -169,7 +169,6 @@ import {
|
||||
} from "./navigation.ts";
|
||||
import { isPluginEnabledInConfigSnapshot } from "./plugin-activation.ts";
|
||||
import { isCronSessionKey, resolveSessionDisplayName } from "./session-display.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import {
|
||||
buildAgentMainSessionKey,
|
||||
isSessionKeyTiedToAgent,
|
||||
@@ -178,10 +177,17 @@ import {
|
||||
parseAgentSessionKey,
|
||||
resolveAgentIdFromSessionKey,
|
||||
} from "./session-key.ts";
|
||||
import "./components/dashboard-header.ts";
|
||||
import type { SidebarContent } from "./sidebar-content.ts";
|
||||
import { loadLocalAssistantIdentity } from "./storage.ts";
|
||||
import { normalizeStringEntries } from "./string-coerce.ts";
|
||||
import { normalizeOptionalString } from "./string-coerce.ts";
|
||||
import type { AgentsFilesGetResult, AgentsFilesListResult, GatewaySessionRow } from "./types.ts";
|
||||
import type {
|
||||
ArtifactDownloadResult,
|
||||
GatewaySessionRow,
|
||||
SessionWorkspaceGetResult,
|
||||
SessionWorkspaceListResult,
|
||||
} from "./types.ts";
|
||||
import { isRenderableControlUiAvatarUrl } from "./views/agents-utils.ts";
|
||||
import { agentLogoUrl } from "./views/agents-utils.ts";
|
||||
import {
|
||||
@@ -678,34 +684,48 @@ const lazyUsage = createLazyView(() => import("./views/usage.ts"), notifyLazyVie
|
||||
const lazyWorkboard = createLazyView(() => import("./views/workboard.ts"), notifyLazyViewChanged);
|
||||
|
||||
type ChatWorkspaceFilesState = {
|
||||
activeName: string | null;
|
||||
activeId: string | null;
|
||||
agentId: string;
|
||||
browserPath: string;
|
||||
browserSearch: string;
|
||||
browserSearchTimer: ReturnType<typeof globalThis.setTimeout> | null;
|
||||
collapsed: boolean;
|
||||
error: string | null;
|
||||
list: AgentsFilesListResult | null;
|
||||
list: SessionWorkspaceListResult | null;
|
||||
loading: boolean;
|
||||
pendingReload: boolean;
|
||||
requestId: number;
|
||||
sessionKey: string;
|
||||
};
|
||||
|
||||
const chatWorkspaceFilesStates = new WeakMap<AppViewState, ChatWorkspaceFilesState>();
|
||||
const chatWorkspaceFileOpenRequests = new WeakMap<
|
||||
AppViewState,
|
||||
{ agentId: string; id: number; name: string; sessionKey: string }
|
||||
{ agentId: string; id: number; itemId: string; sessionKey: string }
|
||||
>();
|
||||
|
||||
function getChatWorkspaceFilesState(state: AppViewState, agentId: string): ChatWorkspaceFilesState {
|
||||
function getChatWorkspaceFilesState(
|
||||
state: AppViewState,
|
||||
sessionKey: string,
|
||||
agentId: string,
|
||||
): ChatWorkspaceFilesState {
|
||||
const current = chatWorkspaceFilesStates.get(state);
|
||||
if (current?.agentId === agentId) {
|
||||
if (current?.sessionKey === sessionKey && current.agentId === agentId) {
|
||||
return current;
|
||||
}
|
||||
const next = {
|
||||
activeName: null,
|
||||
activeId: null,
|
||||
agentId,
|
||||
browserPath: "",
|
||||
browserSearch: "",
|
||||
browserSearchTimer: null,
|
||||
collapsed: true,
|
||||
error: null,
|
||||
list: null,
|
||||
loading: false,
|
||||
pendingReload: false,
|
||||
requestId: 0,
|
||||
sessionKey,
|
||||
};
|
||||
chatWorkspaceFilesStates.set(state, next);
|
||||
return next;
|
||||
@@ -1279,14 +1299,78 @@ function renderCronQuickCreateForTab(
|
||||
});
|
||||
}
|
||||
|
||||
function languageForWorkspaceFile(name: string): string {
|
||||
const extension = name.match(/\.([a-z0-9_-]+)$/i)?.[1]?.toLowerCase() ?? "";
|
||||
if (extension === "json") {
|
||||
return "json";
|
||||
}
|
||||
if (extension === "mdx") {
|
||||
return "mdx";
|
||||
}
|
||||
if (extension === "tsx" || extension === "jsx") {
|
||||
return extension;
|
||||
}
|
||||
if (extension === "ts" || extension === "js" || extension === "css" || extension === "html") {
|
||||
return extension;
|
||||
}
|
||||
if (extension === "yaml" || extension === "yml") {
|
||||
return "yaml";
|
||||
}
|
||||
if (extension === "toml" || extension === "xml" || extension === "svg") {
|
||||
return extension;
|
||||
}
|
||||
return extension;
|
||||
}
|
||||
|
||||
function buildWorkspaceFileSidebarContent(name: string, content: string): string {
|
||||
if (/\.(?:md|markdown|mdx)$/i.test(name)) {
|
||||
return content;
|
||||
}
|
||||
const language = name.match(/\.([a-z0-9_-]+)$/i)?.[1]?.toLowerCase() ?? "";
|
||||
const language = languageForWorkspaceFile(name);
|
||||
return `# ${name}\n\n\`\`\`${language}\n${content}\n\`\`\``;
|
||||
}
|
||||
|
||||
function buildArtifactSidebarContent(params: {
|
||||
data?: string;
|
||||
encoding?: string;
|
||||
mimeType: string;
|
||||
title: string;
|
||||
url?: string;
|
||||
}): SidebarContent {
|
||||
const { data, encoding, mimeType, title, url } = params;
|
||||
if (encoding === "base64" && data && mimeType.startsWith("image/")) {
|
||||
return {
|
||||
kind: "image",
|
||||
title,
|
||||
src: `data:${mimeType};base64,${data}`,
|
||||
mimeType,
|
||||
rawText: url ?? null,
|
||||
};
|
||||
}
|
||||
if (encoding === "base64" && data && mimeType === "application/json") {
|
||||
const decoded = globalThis.atob(data);
|
||||
return {
|
||||
kind: "markdown",
|
||||
content: `# ${title}\n\n\`\`\`json\n${decoded}\n\`\`\``,
|
||||
rawText: decoded,
|
||||
};
|
||||
}
|
||||
if (encoding === "base64" && data && mimeType.startsWith("text/")) {
|
||||
const decoded = globalThis.atob(data);
|
||||
return {
|
||||
kind: "markdown",
|
||||
content: `# ${title}\n\n\`\`\`\n${decoded}\n\`\`\``,
|
||||
rawText: decoded,
|
||||
};
|
||||
}
|
||||
if (url) {
|
||||
const content = `# ${title}\n\n[Open artifact](${url})`;
|
||||
return { kind: "markdown", content, rawText: content };
|
||||
}
|
||||
const content = `# ${title}\n\nArtifact download is not previewable in the sidebar.`;
|
||||
return { kind: "markdown", content, rawText: content };
|
||||
}
|
||||
|
||||
export function renderApp(state: AppViewState) {
|
||||
const updatableState = state as AppViewState & { requestUpdate?: () => void };
|
||||
const requestHostUpdate =
|
||||
@@ -1495,11 +1579,15 @@ export function renderApp(state: AppViewState) {
|
||||
? (scopedChatAgentId ?? chatFallbackAgentId)
|
||||
: (activeSessionAgentId ?? scopedChatAgentId ?? chatFallbackAgentId);
|
||||
const toolsPanelUsesActiveSession = Boolean(resolvedAgentId && resolvedAgentId === chatAgentId);
|
||||
const chatWorkspaceFiles = getChatWorkspaceFilesState(state, chatAgentId);
|
||||
const chatWorkspaceAgentId = resolveChatWorkspaceAgentId();
|
||||
const chatWorkspaceFiles = getChatWorkspaceFilesState(
|
||||
state,
|
||||
state.sessionKey,
|
||||
chatWorkspaceAgentId,
|
||||
);
|
||||
const currentChatWorkspaceFilesState = () =>
|
||||
resolveChatWorkspaceAgentId() === chatAgentId
|
||||
? getChatWorkspaceFilesState(state, chatAgentId)
|
||||
: null;
|
||||
getChatWorkspaceFilesState(state, state.sessionKey, resolveChatWorkspaceAgentId());
|
||||
const currentSessionWorkspaceKey = () => state.sessionKey;
|
||||
const getCurrentConfigValue = () =>
|
||||
state.configForm ?? (state.configSnapshot?.config as Record<string, unknown> | null);
|
||||
const findAgentIndex = (agentId: string) =>
|
||||
@@ -2105,13 +2193,13 @@ export function renderApp(state: AppViewState) {
|
||||
state.agentsList &&
|
||||
!chatWorkspaceFiles.loading &&
|
||||
!chatWorkspaceFiles.error &&
|
||||
chatWorkspaceFiles.list?.agentId !== chatAgentId
|
||||
chatWorkspaceFiles.list?.sessionKey !== state.sessionKey
|
||||
) {
|
||||
loadChatWorkspaceFiles();
|
||||
}
|
||||
const toggleChatWorkspaceFilesCollapsed = () => {
|
||||
chatWorkspaceFiles.collapsed = !chatWorkspaceFiles.collapsed;
|
||||
if (!chatWorkspaceFiles.collapsed && chatWorkspaceFiles.list?.agentId !== chatAgentId) {
|
||||
if (!chatWorkspaceFiles.collapsed && chatWorkspaceFiles.list?.sessionKey !== state.sessionKey) {
|
||||
loadChatWorkspaceFiles();
|
||||
}
|
||||
requestHostUpdate?.();
|
||||
@@ -2119,8 +2207,36 @@ export function renderApp(state: AppViewState) {
|
||||
const refreshChatWorkspaceFiles = () => {
|
||||
loadChatWorkspaceFiles({ force: true });
|
||||
};
|
||||
const browseChatWorkspacePath = (path: string) => {
|
||||
if (chatWorkspaceFiles.browserSearchTimer) {
|
||||
globalThis.clearTimeout(chatWorkspaceFiles.browserSearchTimer);
|
||||
chatWorkspaceFiles.browserSearchTimer = null;
|
||||
}
|
||||
chatWorkspaceFiles.browserPath = path;
|
||||
chatWorkspaceFiles.browserSearch = "";
|
||||
loadChatWorkspaceFiles({ force: true });
|
||||
};
|
||||
const searchChatWorkspaceFiles = (search: string) => {
|
||||
chatWorkspaceFiles.browserSearch = search;
|
||||
if (chatWorkspaceFiles.browserSearchTimer) {
|
||||
globalThis.clearTimeout(chatWorkspaceFiles.browserSearchTimer);
|
||||
}
|
||||
chatWorkspaceFiles.browserSearchTimer = globalThis.setTimeout(() => {
|
||||
chatWorkspaceFiles.browserSearchTimer = null;
|
||||
loadChatWorkspaceFiles({ force: true });
|
||||
}, 160);
|
||||
};
|
||||
const copyChatWorkspacePath = (filePath: string) => {
|
||||
void globalThis.navigator?.clipboard?.writeText?.(filePath);
|
||||
};
|
||||
function loadChatWorkspaceFiles(opts?: { force?: boolean }) {
|
||||
if (!state.client || !state.connected || chatWorkspaceFiles.loading) {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
if (chatWorkspaceFiles.loading) {
|
||||
if (opts?.force) {
|
||||
chatWorkspaceFiles.pendingReload = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
const requestId = chatWorkspaceFiles.requestId + 1;
|
||||
@@ -2131,18 +2247,45 @@ export function renderApp(state: AppViewState) {
|
||||
chatWorkspaceFiles.list = null;
|
||||
}
|
||||
const requestState = chatWorkspaceFiles;
|
||||
requestState.pendingReload = false;
|
||||
const sessionKey = state.sessionKey;
|
||||
const agentId = chatWorkspaceFiles.agentId;
|
||||
void (async () => {
|
||||
try {
|
||||
const res = await state.client?.request<AgentsFilesListResult | null>("agents.files.list", {
|
||||
agentId: chatAgentId,
|
||||
const res = await state.client?.request<SessionWorkspaceListResult | null>(
|
||||
"sessions.files.list",
|
||||
{
|
||||
sessionKey,
|
||||
path: requestState.browserSearch ? "" : requestState.browserPath,
|
||||
search: requestState.browserSearch,
|
||||
...(agentId ? { agentId } : {}),
|
||||
},
|
||||
);
|
||||
const artifacts = await state.client?.request<{
|
||||
artifacts?: SessionWorkspaceListResult["artifacts"];
|
||||
} | null>("artifacts.list", {
|
||||
sessionKey,
|
||||
...(agentId ? { agentId } : {}),
|
||||
});
|
||||
const current = currentChatWorkspaceFilesState();
|
||||
if (current !== requestState || current.requestId !== requestId) {
|
||||
return;
|
||||
}
|
||||
current.list = res ?? null;
|
||||
if (current.activeName && !res?.files.some((file) => file.name === current.activeName)) {
|
||||
current.activeName = null;
|
||||
const files = res?.files ?? [];
|
||||
const artifactItems = artifacts?.artifacts ?? [];
|
||||
current.list = {
|
||||
sessionKey,
|
||||
...(res?.root ? { root: res.root } : {}),
|
||||
files,
|
||||
...(res?.browser ? { browser: res.browser } : {}),
|
||||
artifacts: artifactItems,
|
||||
};
|
||||
if (
|
||||
current.activeId &&
|
||||
!files.some((file) => `file:${file.path}` === current.activeId) &&
|
||||
!artifactItems.some((artifact) => `artifact:${artifact.id}` === current.activeId)
|
||||
) {
|
||||
current.activeId = null;
|
||||
}
|
||||
} catch (err) {
|
||||
const current = currentChatWorkspaceFilesState();
|
||||
@@ -2153,19 +2296,25 @@ export function renderApp(state: AppViewState) {
|
||||
const current = currentChatWorkspaceFilesState();
|
||||
if (current === requestState && current.requestId === requestId) {
|
||||
current.loading = false;
|
||||
const shouldReload = current.pendingReload;
|
||||
current.pendingReload = false;
|
||||
if (shouldReload) {
|
||||
loadChatWorkspaceFiles({ force: true });
|
||||
}
|
||||
}
|
||||
requestHostUpdate?.();
|
||||
}
|
||||
})();
|
||||
}
|
||||
const openChatWorkspaceFile = (name: string) => {
|
||||
chatWorkspaceFiles.activeName = name;
|
||||
const openChatWorkspaceFile = (filePath: string) => {
|
||||
const itemId = `file:${filePath}`;
|
||||
chatWorkspaceFiles.activeId = itemId;
|
||||
const previousRequest = chatWorkspaceFileOpenRequests.get(state);
|
||||
const openRequest = {
|
||||
agentId: chatAgentId,
|
||||
agentId: chatWorkspaceFiles.agentId,
|
||||
id: (previousRequest?.id ?? 0) + 1,
|
||||
name,
|
||||
sessionKey: state.sessionKey,
|
||||
itemId,
|
||||
sessionKey: currentSessionWorkspaceKey(),
|
||||
};
|
||||
chatWorkspaceFileOpenRequests.set(state, openRequest);
|
||||
const isCurrentOpenRequest = () => {
|
||||
@@ -2174,9 +2323,10 @@ export function renderApp(state: AppViewState) {
|
||||
return (
|
||||
currentRequest?.id === openRequest.id &&
|
||||
currentRequest.agentId === resolveChatWorkspaceAgentId() &&
|
||||
currentRequest.name === name &&
|
||||
currentRequest.sessionKey === state.sessionKey &&
|
||||
currentFiles?.activeName === name
|
||||
currentRequest.itemId === itemId &&
|
||||
currentRequest.sessionKey === currentSessionWorkspaceKey() &&
|
||||
currentFiles?.agentId === openRequest.agentId &&
|
||||
currentFiles?.activeId === itemId
|
||||
);
|
||||
};
|
||||
void (async () => {
|
||||
@@ -2185,14 +2335,82 @@ export function renderApp(state: AppViewState) {
|
||||
}
|
||||
chatWorkspaceFiles.error = null;
|
||||
try {
|
||||
const res = await state.client.request<AgentsFilesGetResult | null>("agents.files.get", {
|
||||
agentId: chatAgentId,
|
||||
name,
|
||||
});
|
||||
const content = res?.file?.content;
|
||||
if (typeof content !== "string") {
|
||||
const agentId = openRequest.agentId;
|
||||
const res = await state.client.request<SessionWorkspaceGetResult | null>(
|
||||
"sessions.files.get",
|
||||
{
|
||||
sessionKey: openRequest.sessionKey,
|
||||
path: filePath,
|
||||
...(agentId ? { agentId } : {}),
|
||||
},
|
||||
);
|
||||
const file = res?.file;
|
||||
if (!file || typeof file.content !== "string") {
|
||||
if (isCurrentOpenRequest()) {
|
||||
chatWorkspaceFiles.error = `Failed to load ${name}`;
|
||||
chatWorkspaceFiles.error = `Failed to load ${filePath}`;
|
||||
requestHostUpdate?.();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const content = file.content;
|
||||
if (!isCurrentOpenRequest()) {
|
||||
return;
|
||||
}
|
||||
state.handleOpenSidebar({
|
||||
kind: "markdown",
|
||||
content: buildWorkspaceFileSidebarContent(file.name || filePath, content),
|
||||
rawText: content,
|
||||
});
|
||||
} catch (err) {
|
||||
if (isCurrentOpenRequest()) {
|
||||
chatWorkspaceFiles.error = String(err);
|
||||
}
|
||||
} finally {
|
||||
requestHostUpdate?.();
|
||||
}
|
||||
})();
|
||||
};
|
||||
const openChatWorkspaceArtifact = (artifactId: string) => {
|
||||
const itemId = `artifact:${artifactId}`;
|
||||
chatWorkspaceFiles.activeId = itemId;
|
||||
const previousRequest = chatWorkspaceFileOpenRequests.get(state);
|
||||
const openRequest = {
|
||||
agentId: chatWorkspaceFiles.agentId,
|
||||
id: (previousRequest?.id ?? 0) + 1,
|
||||
itemId,
|
||||
sessionKey: currentSessionWorkspaceKey(),
|
||||
};
|
||||
chatWorkspaceFileOpenRequests.set(state, openRequest);
|
||||
const isCurrentOpenRequest = () => {
|
||||
const currentRequest = chatWorkspaceFileOpenRequests.get(state);
|
||||
const currentFiles = currentChatWorkspaceFilesState();
|
||||
return (
|
||||
currentRequest?.id === openRequest.id &&
|
||||
currentRequest.agentId === resolveChatWorkspaceAgentId() &&
|
||||
currentRequest.itemId === itemId &&
|
||||
currentRequest.sessionKey === currentSessionWorkspaceKey() &&
|
||||
currentFiles?.agentId === openRequest.agentId &&
|
||||
currentFiles?.activeId === itemId
|
||||
);
|
||||
};
|
||||
void (async () => {
|
||||
if (!state.client || !state.connected) {
|
||||
return;
|
||||
}
|
||||
chatWorkspaceFiles.error = null;
|
||||
try {
|
||||
const agentId = openRequest.agentId;
|
||||
const res = await state.client.request<ArtifactDownloadResult | null>(
|
||||
"artifacts.download",
|
||||
{
|
||||
sessionKey: openRequest.sessionKey,
|
||||
artifactId,
|
||||
...(agentId ? { agentId } : {}),
|
||||
},
|
||||
);
|
||||
if (!res?.artifact) {
|
||||
if (isCurrentOpenRequest()) {
|
||||
chatWorkspaceFiles.error = `Failed to load artifact ${artifactId}`;
|
||||
requestHostUpdate?.();
|
||||
}
|
||||
return;
|
||||
@@ -2200,11 +2418,16 @@ export function renderApp(state: AppViewState) {
|
||||
if (!isCurrentOpenRequest()) {
|
||||
return;
|
||||
}
|
||||
state.handleOpenSidebar({
|
||||
kind: "markdown",
|
||||
content: buildWorkspaceFileSidebarContent(name, content),
|
||||
rawText: content,
|
||||
const title = res.artifact.title;
|
||||
const mimeType = res.artifact.mimeType ?? "";
|
||||
const preview = buildArtifactSidebarContent({
|
||||
data: res.data,
|
||||
encoding: res.encoding,
|
||||
mimeType,
|
||||
title,
|
||||
url: res.url,
|
||||
});
|
||||
state.handleOpenSidebar(preview);
|
||||
} catch (err) {
|
||||
if (isCurrentOpenRequest()) {
|
||||
chatWorkspaceFiles.error = String(err);
|
||||
@@ -3543,19 +3766,23 @@ export function renderApp(state: AppViewState) {
|
||||
onDismissError: () => dismissChatError(state),
|
||||
sessions: state.sessionsResult,
|
||||
composerControls: renderGuardedChatControls(state),
|
||||
workspaceFiles: {
|
||||
sessionWorkspace: {
|
||||
collapsed: chatWorkspaceFiles.collapsed,
|
||||
agentId: chatAgentId,
|
||||
sessionKey: state.sessionKey,
|
||||
list:
|
||||
chatWorkspaceFiles.list?.agentId === chatAgentId
|
||||
chatWorkspaceFiles.list?.sessionKey === state.sessionKey
|
||||
? chatWorkspaceFiles.list
|
||||
: null,
|
||||
loading: chatWorkspaceFiles.loading,
|
||||
error: chatWorkspaceFiles.error,
|
||||
activeName: chatWorkspaceFiles.activeName,
|
||||
activeId: chatWorkspaceFiles.activeId,
|
||||
onToggleCollapsed: toggleChatWorkspaceFilesCollapsed,
|
||||
onRefresh: refreshChatWorkspaceFiles,
|
||||
onBrowsePath: browseChatWorkspacePath,
|
||||
onCopyPath: copyChatWorkspacePath,
|
||||
onOpenFile: openChatWorkspaceFile,
|
||||
onSearch: searchChatWorkspaceFiles,
|
||||
onOpenArtifact: openChatWorkspaceArtifact,
|
||||
},
|
||||
autoExpandToolCalls: false,
|
||||
onRefresh: () => {
|
||||
|
||||
@@ -27,7 +27,7 @@ function iconSvg() {
|
||||
return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"></path></svg>`;
|
||||
}
|
||||
|
||||
function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) {
|
||||
function sidebarSessionPickerHtml(opts: { sidebarOpen?: boolean; workspaceRail?: boolean } = {}) {
|
||||
const optionButtons = Array.from({ length: 18 }, (_, index) => {
|
||||
const sessionKey = `dashboard-session-${index + 1}`;
|
||||
const selected = index === 0;
|
||||
@@ -53,7 +53,7 @@ function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) {
|
||||
}).join("");
|
||||
const workspaceRail = opts.workspaceRail
|
||||
? `
|
||||
<aside class="chat-workspace-rail" aria-label="Workspace files">
|
||||
<aside class="chat-workspace-rail" aria-label="Session workspace">
|
||||
<div class="chat-workspace-rail__header">
|
||||
<div class="chat-workspace-rail__title">
|
||||
<span class="chat-workspace-rail__eyebrow">Workspace</span>
|
||||
@@ -65,20 +65,24 @@ function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) {
|
||||
</div>
|
||||
<div class="chat-workspace-rail__path">/workspace/openclaw</div>
|
||||
<div class="chat-workspace-rail__list" role="list">
|
||||
<button class="chat-workspace-rail__file chat-workspace-rail__file--active" type="button" role="listitem">
|
||||
<span class="chat-workspace-rail__file-icon">${iconSvg()}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">ui/src/ui/chat/session-controls.ts</span>
|
||||
<span class="chat-workspace-rail__file-meta">24 KB</span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="chat-workspace-rail__file" type="button" role="listitem">
|
||||
<span class="chat-workspace-rail__file-icon">${iconSvg()}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">ui/src/styles/layout.css</span>
|
||||
<span class="chat-workspace-rail__file-meta">31 KB</span>
|
||||
</span>
|
||||
</button>
|
||||
<div class="chat-workspace-rail__file chat-workspace-rail__file--active" role="listitem">
|
||||
<button class="chat-workspace-rail__file-open" type="button">
|
||||
<span class="chat-workspace-rail__file-icon">${iconSvg()}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">ui/src/ui/chat/session-controls.ts</span>
|
||||
<span class="chat-workspace-rail__file-meta">24 KB</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="chat-workspace-rail__file" role="listitem">
|
||||
<button class="chat-workspace-rail__file-open" type="button">
|
||||
<span class="chat-workspace-rail__file-icon">${iconSvg()}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">ui/src/styles/layout.css</span>
|
||||
<span class="chat-workspace-rail__file-meta">31 KB</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
`
|
||||
@@ -157,32 +161,65 @@ function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) {
|
||||
<main class="content content--chat">
|
||||
<section class="card chat">
|
||||
<div class="chat-workbench">
|
||||
<div class="chat-split-container">
|
||||
<div class="chat-main">
|
||||
<div class="chat-thread" role="log">
|
||||
<div class="chat-thread-inner">
|
||||
<div class="chat-group assistant">
|
||||
<div class="chat-group-messages">
|
||||
<div class="chat-bubble">
|
||||
<div class="chat-text">
|
||||
<p>Keep the sidebar session picker interactive even when the desktop chat workbench is visible.</p>
|
||||
${workspaceRail}
|
||||
<div class="chat-workbench__main">
|
||||
<div class="chat-split-container${opts.sidebarOpen ? " chat-split-container--open" : ""}">
|
||||
<div class="chat-main" style="flex: ${opts.sidebarOpen ? "0 1 72%" : "1 1 100%"}">
|
||||
<div class="chat-thread" role="log">
|
||||
<div class="chat-thread-inner">
|
||||
<div class="chat-group assistant">
|
||||
<div class="chat-group-messages">
|
||||
<div class="chat-bubble">
|
||||
<div class="chat-text">
|
||||
<p>Keep the sidebar session picker interactive even when the desktop chat workbench is visible.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:28px 0 0;padding:0 8px;"
|
||||
>
|
||||
<button class="btn" type="button">What can you do?</button>
|
||||
<button class="btn" type="button">Summarize recent sessions</button>
|
||||
<button class="btn" type="button">Help me configure a channel</button>
|
||||
<button class="btn" type="button">Check system health</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin:28px 0 0;padding:0 8px;"
|
||||
>
|
||||
<button class="btn" type="button">What can you do?</button>
|
||||
<button class="btn" type="button">Summarize recent sessions</button>
|
||||
<button class="btn" type="button">Help me configure a channel</button>
|
||||
<button class="btn" type="button">Check system health</button>
|
||||
</div>
|
||||
<div class="agent-chat__input">
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea placeholder="Message OpenClaw"></textarea>
|
||||
</div>
|
||||
<div class="agent-chat__toolbar">
|
||||
<div class="agent-chat__toolbar-left">
|
||||
<button class="agent-chat__input-btn" type="button" aria-label="Attach file">
|
||||
${iconSvg()}
|
||||
</button>
|
||||
</div>
|
||||
<button class="btn btn--ghost" type="button">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${
|
||||
opts.sidebarOpen
|
||||
? `
|
||||
<resizable-divider style="width:14px;flex:0 0 14px;"></resizable-divider>
|
||||
<div class="chat-sidebar">
|
||||
<div class="sidebar-panel">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-title">AGENTS.md</div>
|
||||
<button class="btn" type="button">Close</button>
|
||||
</div>
|
||||
<div class="sidebar-content">
|
||||
<article class="sidebar-markdown">File preview content</article>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
${workspaceRail}
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
@@ -206,7 +243,7 @@ function sidebarSessionPickerHtml(opts: { workspaceRail?: boolean } = {}) {
|
||||
async function openSidebarSessionPickerFixture(
|
||||
width: number,
|
||||
height: number,
|
||||
opts: { workspaceRail?: boolean } = {},
|
||||
opts: { sidebarOpen?: boolean; workspaceRail?: boolean } = {},
|
||||
): Promise<Page> {
|
||||
const page = await browser.newPage({ viewport: { width, height } });
|
||||
await page.setContent(
|
||||
@@ -289,4 +326,43 @@ describeBrowserLayout("sidebar session picker browser layout", () => {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the file preview sidebar inside the main workbench column when the workspace rail is visible", async () => {
|
||||
const page = await openSidebarSessionPickerFixture(1366, 900, {
|
||||
sidebarOpen: true,
|
||||
workspaceRail: true,
|
||||
});
|
||||
try {
|
||||
await expectNoHorizontalOverflow(page);
|
||||
const boxes = await page.evaluate(() => {
|
||||
const rectFor = (selector: string) => {
|
||||
const element = document.querySelector(selector);
|
||||
if (!element) {
|
||||
throw new Error(`Missing ${selector}`);
|
||||
}
|
||||
const rect = element.getBoundingClientRect();
|
||||
return {
|
||||
left: rect.left,
|
||||
right: rect.right,
|
||||
width: rect.width,
|
||||
};
|
||||
};
|
||||
return {
|
||||
main: rectFor(".chat-workbench__main"),
|
||||
input: rectFor(".agent-chat__input"),
|
||||
rail: rectFor(".chat-workspace-rail"),
|
||||
sidebar: rectFor(".chat-sidebar"),
|
||||
split: rectFor(".chat-split-container"),
|
||||
};
|
||||
});
|
||||
|
||||
expect(boxes.split.right).toBeLessThanOrEqual(boxes.rail.left + 1);
|
||||
expect(boxes.sidebar.right).toBeLessThanOrEqual(boxes.rail.left + 1);
|
||||
expect(boxes.input.right).toBeLessThanOrEqual(boxes.sidebar.left + 1);
|
||||
expect(boxes.sidebar.left).toBeGreaterThanOrEqual(boxes.main.left - 1);
|
||||
expect(boxes.sidebar.width).toBeGreaterThanOrEqual(300);
|
||||
} finally {
|
||||
await page.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -230,43 +230,86 @@ describeControlUiE2e("Control UI mocked Gateway E2E", () => {
|
||||
const page = await context.newPage();
|
||||
const gateway = await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"agents.files.list": {
|
||||
agentId: "main",
|
||||
files: [{ name: "AGENTS.md", path: "/workspace/AGENTS.md", size: 2048 }],
|
||||
workspace: "/workspace",
|
||||
"artifacts.list": {
|
||||
artifacts: [
|
||||
{
|
||||
download: { mode: "bytes" },
|
||||
id: "artifact-1",
|
||||
mimeType: "image/png",
|
||||
sizeBytes: 128,
|
||||
title: "preview.png",
|
||||
type: "image",
|
||||
},
|
||||
],
|
||||
},
|
||||
"sessions.files.list": {
|
||||
browser: {
|
||||
entries: [
|
||||
{
|
||||
kind: "directory",
|
||||
name: "src",
|
||||
path: "src",
|
||||
sessionKind: "modified",
|
||||
},
|
||||
{
|
||||
kind: "file",
|
||||
name: "package.json",
|
||||
path: "package.json",
|
||||
size: 4096,
|
||||
},
|
||||
],
|
||||
path: "",
|
||||
},
|
||||
files: [
|
||||
{
|
||||
kind: "modified",
|
||||
missing: false,
|
||||
name: "AGENTS.md",
|
||||
path: "/workspace/AGENTS.md",
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
root: "/workspace",
|
||||
sessionKey: "main",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await page.goto(`${server.baseUrl}chat`);
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).waitFor({
|
||||
await page.getByRole("button", { name: "Expand session workspace" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(await gateway.getRequests("agents.files.list")).toHaveLength(0);
|
||||
expect(await gateway.getRequests("sessions.files.list")).toHaveLength(0);
|
||||
expect(await page.locator(".chat-workspace-rail__file").count()).toBe(0);
|
||||
expect(await page.locator(".chat-workspace-rail__collapsed-icon svg").count()).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).click();
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).waitFor({
|
||||
await page.getByRole("button", { name: "Expand session workspace" }).click();
|
||||
await page.getByRole("button", { name: "Collapse session workspace" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByText("AGENTS.md").waitFor({ timeout: 10_000 });
|
||||
expect(await gateway.getRequests("agents.files.list")).toHaveLength(1);
|
||||
await page.getByText("preview.png").waitFor({ timeout: 10_000 });
|
||||
await page.getByText("Project files").waitFor({ timeout: 10_000 });
|
||||
await page.locator(".chat-workspace-rail__file-name", { hasText: "package.json" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(await gateway.getRequests("sessions.files.list")).toHaveLength(1);
|
||||
expect(await gateway.getRequests("artifacts.list")).toHaveLength(1);
|
||||
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).click();
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).waitFor({
|
||||
await page.getByRole("button", { name: "Collapse session workspace" }).click();
|
||||
await page.getByRole("button", { name: "Expand session workspace" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
expect(await page.locator(".chat-workspace-rail__file").count()).toBe(0);
|
||||
expect(await page.locator(".chat-workspace-rail__collapsed-icon svg").count()).toBe(1);
|
||||
|
||||
await page.getByRole("button", { name: "Expand workspace files" }).click();
|
||||
await page.getByRole("button", { name: "Collapse workspace files" }).waitFor({
|
||||
await page.getByRole("button", { name: "Expand session workspace" }).click();
|
||||
await page.getByRole("button", { name: "Collapse session workspace" }).waitFor({
|
||||
timeout: 10_000,
|
||||
});
|
||||
await page.getByText("AGENTS.md").waitFor({ timeout: 10_000 });
|
||||
expect(await gateway.getRequests("agents.files.list")).toHaveLength(1);
|
||||
expect(await gateway.getRequests("sessions.files.list")).toHaveLength(1);
|
||||
|
||||
await page.setViewportSize({ height: 900, width: 1000 });
|
||||
expect(await page.locator(".chat-workspace-rail").isHidden()).toBe(true);
|
||||
|
||||
@@ -25,4 +25,14 @@ export type CanvasSidebarContent = {
|
||||
unavailableReason?: "not_found" | "oversized" | "not_visible" | null;
|
||||
};
|
||||
|
||||
export type SidebarContent = MarkdownSidebarContent | CanvasSidebarContent;
|
||||
export type ImageSidebarContent = {
|
||||
kind: "image";
|
||||
title: string;
|
||||
src: string;
|
||||
mimeType?: string | null;
|
||||
rawText?: string | null;
|
||||
fullMessageRequest?: SidebarFullMessageRequest;
|
||||
unavailableReason?: "not_found" | "oversized" | "not_visible" | null;
|
||||
};
|
||||
|
||||
export type SidebarContent = MarkdownSidebarContent | CanvasSidebarContent | ImageSidebarContent;
|
||||
|
||||
@@ -387,6 +387,66 @@ export type AgentsFilesSetResult = {
|
||||
file: AgentFileEntry;
|
||||
};
|
||||
|
||||
export type SessionWorkspaceFileEntry = {
|
||||
path: string;
|
||||
name: string;
|
||||
kind: "modified" | "read";
|
||||
missing: boolean;
|
||||
size?: number;
|
||||
updatedAtMs?: number;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
export type SessionWorkspaceBrowserEntry = {
|
||||
path: string;
|
||||
name: string;
|
||||
kind: "file" | "directory";
|
||||
sessionKind?: "modified" | "read" | "mixed";
|
||||
size?: number;
|
||||
updatedAtMs?: number;
|
||||
};
|
||||
|
||||
export type SessionWorkspaceBrowserResult = {
|
||||
path: string;
|
||||
parentPath?: string;
|
||||
search?: string;
|
||||
entries: SessionWorkspaceBrowserEntry[];
|
||||
truncated?: boolean;
|
||||
};
|
||||
|
||||
export type SessionWorkspaceArtifactEntry = {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
mimeType?: string;
|
||||
sizeBytes?: number;
|
||||
source?: string;
|
||||
download: {
|
||||
mode: "bytes" | "url" | "unsupported";
|
||||
};
|
||||
};
|
||||
|
||||
export type SessionWorkspaceListResult = {
|
||||
sessionKey: string;
|
||||
root?: string;
|
||||
files: SessionWorkspaceFileEntry[];
|
||||
browser?: SessionWorkspaceBrowserResult;
|
||||
artifacts?: SessionWorkspaceArtifactEntry[];
|
||||
};
|
||||
|
||||
export type SessionWorkspaceGetResult = {
|
||||
sessionKey: string;
|
||||
root?: string;
|
||||
file: SessionWorkspaceFileEntry;
|
||||
};
|
||||
|
||||
export type ArtifactDownloadResult = {
|
||||
artifact: SessionWorkspaceArtifactEntry;
|
||||
encoding?: "base64";
|
||||
data?: string;
|
||||
url?: string;
|
||||
};
|
||||
|
||||
export type SessionRunStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
||||
export type SubagentRunState = "active" | "interrupted" | "historical";
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import type { GatewayBrowserClient } from "../gateway.ts";
|
||||
import type { GatewaySessionRow, ModelCatalogEntry, SessionsListResult } from "../types.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../ui-types.ts";
|
||||
import { renderChat, resetChatViewState } from "./chat.ts";
|
||||
import { renderMarkdownSidebar } from "./markdown-sidebar.ts";
|
||||
|
||||
const refreshVisibleToolsEffectiveForCurrentSessionMock = vi.hoisted(() =>
|
||||
vi.fn(async (state: AppViewState) => {
|
||||
@@ -878,30 +879,56 @@ describe("chat composer workbench", () => {
|
||||
it("renders session controls in the composer and workspace files in the expanded rail", () => {
|
||||
const onToggleCollapsed = vi.fn();
|
||||
const onRefresh = vi.fn();
|
||||
const onBrowsePath = vi.fn();
|
||||
const onCopyPath = vi.fn();
|
||||
const onOpenFile = vi.fn();
|
||||
const onSearch = vi.fn();
|
||||
const container = renderChatView({
|
||||
composerControls: html`<button class="test-composer-control">Model</button>`,
|
||||
workspaceFiles: {
|
||||
sessionWorkspace: {
|
||||
collapsed: false,
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main",
|
||||
list: {
|
||||
agentId: "main",
|
||||
workspace: "/workspace",
|
||||
sessionKey: "agent:main",
|
||||
root: "/workspace",
|
||||
files: [
|
||||
{
|
||||
name: "AGENTS.md",
|
||||
path: "/workspace/AGENTS.md",
|
||||
kind: "modified",
|
||||
missing: false,
|
||||
size: 2048,
|
||||
},
|
||||
],
|
||||
browser: {
|
||||
path: "",
|
||||
entries: [
|
||||
{
|
||||
name: "ui",
|
||||
path: "ui",
|
||||
kind: "directory",
|
||||
sessionKind: "modified",
|
||||
},
|
||||
{
|
||||
name: "package.json",
|
||||
path: "package.json",
|
||||
kind: "file",
|
||||
size: 4096,
|
||||
},
|
||||
],
|
||||
},
|
||||
artifacts: [],
|
||||
},
|
||||
loading: false,
|
||||
error: null,
|
||||
activeName: "AGENTS.md",
|
||||
activeId: "file:/workspace/AGENTS.md",
|
||||
onToggleCollapsed,
|
||||
onRefresh,
|
||||
onBrowsePath,
|
||||
onCopyPath,
|
||||
onOpenFile,
|
||||
onSearch,
|
||||
onOpenArtifact: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -920,40 +947,57 @@ describe("chat composer workbench", () => {
|
||||
expect(container.querySelector(".chat-workspace-rail__path")?.textContent?.trim()).toBe(
|
||||
"/workspace",
|
||||
);
|
||||
const file = container.querySelector<HTMLButtonElement>(".chat-workspace-rail__file");
|
||||
const file = container.querySelector<HTMLDivElement>(".chat-workspace-rail__file");
|
||||
expect(file?.textContent).toContain("AGENTS.md");
|
||||
expect(file?.textContent).toContain("2 KB");
|
||||
expect(container.querySelector(".chat-workspace-rail__summary")?.textContent).toContain(
|
||||
"1 changed",
|
||||
);
|
||||
expect(container.querySelector(".chat-workspace-rail__browser")?.textContent).toContain(
|
||||
"package.json",
|
||||
);
|
||||
|
||||
file?.click();
|
||||
file?.querySelector<HTMLButtonElement>(".chat-workspace-rail__file-open")?.click();
|
||||
file?.querySelector<HTMLButtonElement>('button[aria-label="Copy path"]')?.click();
|
||||
const browserDirectory = Array.from(
|
||||
container.querySelectorAll<HTMLDivElement>(".chat-workspace-rail__file"),
|
||||
).find((row) => row.textContent?.includes("ui"));
|
||||
browserDirectory?.querySelector<HTMLButtonElement>(".chat-workspace-rail__file-open")?.click();
|
||||
container
|
||||
.querySelector<HTMLButtonElement>('button[aria-label="Collapse workspace files"]')
|
||||
.querySelector<HTMLButtonElement>('button[aria-label="Collapse session workspace"]')
|
||||
?.click();
|
||||
|
||||
expect(onOpenFile).toHaveBeenCalledWith("AGENTS.md");
|
||||
expect(onOpenFile).toHaveBeenCalledWith("/workspace/AGENTS.md");
|
||||
expect(onCopyPath).toHaveBeenCalledWith("/workspace/AGENTS.md");
|
||||
expect(onBrowsePath).toHaveBeenCalledWith("ui");
|
||||
expect(onToggleCollapsed).toHaveBeenCalledTimes(1);
|
||||
expect(container.querySelector('button[aria-label="Workspace files"]')).toBeNull();
|
||||
expect(container.querySelector('button[aria-label="Session workspace"]')).toBeNull();
|
||||
});
|
||||
|
||||
it("keeps the workspace files rail reachable from the collapsed strip", () => {
|
||||
const onToggleCollapsed = vi.fn();
|
||||
const container = renderChatView({
|
||||
workspaceFiles: {
|
||||
sessionWorkspace: {
|
||||
collapsed: true,
|
||||
agentId: "main",
|
||||
sessionKey: "agent:main",
|
||||
list: null,
|
||||
loading: false,
|
||||
error: null,
|
||||
activeName: null,
|
||||
activeId: null,
|
||||
onToggleCollapsed,
|
||||
onRefresh: () => undefined,
|
||||
onBrowsePath: () => undefined,
|
||||
onCopyPath: () => undefined,
|
||||
onOpenFile: () => undefined,
|
||||
onSearch: () => undefined,
|
||||
onOpenArtifact: () => undefined,
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector(".chat-workspace-rail__list")).toBeNull();
|
||||
expect(container.querySelector(".chat-workspace-rail__collapsed-icon")).not.toBeNull();
|
||||
const toggle = container.querySelector<HTMLButtonElement>(
|
||||
'button[aria-label="Expand workspace files"]',
|
||||
'button[aria-label="Expand session workspace"]',
|
||||
);
|
||||
expect(toggle?.getAttribute("aria-expanded")).toBe("false");
|
||||
|
||||
@@ -2068,6 +2112,29 @@ describe("chat sidebar raw content", () => {
|
||||
rawText: "Raw",
|
||||
});
|
||||
});
|
||||
|
||||
it("renders image sidebar content as an image instead of markdown text", () => {
|
||||
const container = document.createElement("div");
|
||||
|
||||
render(
|
||||
renderMarkdownSidebar({
|
||||
content: {
|
||||
kind: "image",
|
||||
title: "artifact-preview.png",
|
||||
src: "data:image/png;base64,aW1hZ2U=",
|
||||
mimeType: "image/png",
|
||||
},
|
||||
error: null,
|
||||
onClose: () => undefined,
|
||||
onViewRawText: () => undefined,
|
||||
}),
|
||||
container,
|
||||
);
|
||||
|
||||
const image = container.querySelector<HTMLImageElement>("img.chat-tool-card__preview-image");
|
||||
expect(image?.getAttribute("src")).toBe("data:image/png;base64,aW1hZ2U=");
|
||||
expect(container.textContent).not.toContain("data:image/png;base64");
|
||||
});
|
||||
});
|
||||
|
||||
describe("chat welcome", () => {
|
||||
|
||||
+554
-243
@@ -58,12 +58,7 @@ import { icons } from "../icons.ts";
|
||||
import { formatGoalDetail, formatGoalSummary } from "../session-goal.ts";
|
||||
import type { SidebarContent } from "../sidebar-content.ts";
|
||||
import { detectTextDirection } from "../text-direction.ts";
|
||||
import type {
|
||||
AgentFileEntry,
|
||||
AgentsFilesListResult,
|
||||
SessionGoal,
|
||||
SessionsListResult,
|
||||
} from "../types.ts";
|
||||
import type { SessionWorkspaceListResult, SessionGoal, SessionsListResult } from "../types.ts";
|
||||
import type { ChatAttachment, ChatQueueItem } from "../ui-types.ts";
|
||||
import { resolveLocalUserName } from "../user-identity.ts";
|
||||
import { renderMarkdownSidebar } from "./markdown-sidebar.ts";
|
||||
@@ -198,16 +193,20 @@ export type ChatProps = {
|
||||
onChatScroll?: (event: Event) => void;
|
||||
basePath?: string;
|
||||
composerControls?: TemplateResult | typeof nothing | ReturnType<typeof guard>;
|
||||
workspaceFiles?: {
|
||||
sessionWorkspace?: {
|
||||
collapsed: boolean;
|
||||
agentId: string;
|
||||
list: AgentsFilesListResult | null;
|
||||
sessionKey: string;
|
||||
list: SessionWorkspaceListResult | null;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
activeName: string | null;
|
||||
activeId: string | null;
|
||||
onToggleCollapsed: () => void;
|
||||
onRefresh: () => void;
|
||||
onOpenFile: (name: string) => void;
|
||||
onBrowsePath: (path: string) => void;
|
||||
onCopyPath: (path: string) => void;
|
||||
onOpenFile: (path: string) => void;
|
||||
onSearch: (search: string) => void;
|
||||
onOpenArtifact: (artifactId: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1010,7 +1009,7 @@ function renderChatGoal(goal: SessionGoal | undefined): TemplateResult | typeof
|
||||
`;
|
||||
}
|
||||
|
||||
function formatWorkspaceFileSize(file: AgentFileEntry): string {
|
||||
function formatWorkspaceFileSize(file: { size?: number }): string {
|
||||
const size = file.size;
|
||||
if (typeof size !== "number" || !Number.isFinite(size) || size < 0) {
|
||||
return "";
|
||||
@@ -1024,13 +1023,32 @@ function formatWorkspaceFileSize(file: AgentFileEntry): string {
|
||||
return `${size} B`;
|
||||
}
|
||||
|
||||
function renderWorkspaceFileRail(
|
||||
workspaceFiles: NonNullable<ChatProps["workspaceFiles"]> | undefined,
|
||||
function renderWorkspaceArtifactSize(artifact: { sizeBytes?: number }): string {
|
||||
return formatWorkspaceFileSize({ size: artifact.sizeBytes });
|
||||
}
|
||||
|
||||
function renderWorkspaceRailSection(
|
||||
title: string,
|
||||
content: TemplateResult | typeof nothing,
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!workspaceFiles) {
|
||||
if (content === nothing) {
|
||||
return nothing;
|
||||
}
|
||||
if (workspaceFiles.collapsed) {
|
||||
return html`
|
||||
<section class="chat-workspace-rail__section">
|
||||
<div class="chat-workspace-rail__section-title">${title}</div>
|
||||
${content}
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSessionWorkspaceRail(
|
||||
sessionWorkspace: NonNullable<ChatProps["sessionWorkspace"]> | undefined,
|
||||
): TemplateResult | typeof nothing {
|
||||
if (!sessionWorkspace) {
|
||||
return nothing;
|
||||
}
|
||||
if (sessionWorkspace.collapsed) {
|
||||
return html`
|
||||
<aside
|
||||
class="chat-workspace-rail chat-workspace-rail--collapsed"
|
||||
@@ -1042,7 +1060,7 @@ function renderWorkspaceFileRail(
|
||||
title=${t("chat.workspaceFiles.expand")}
|
||||
aria-label=${t("chat.workspaceFiles.expand")}
|
||||
aria-expanded="false"
|
||||
@click=${workspaceFiles.onToggleCollapsed}
|
||||
@click=${sessionWorkspace.onToggleCollapsed}
|
||||
>
|
||||
<span class="nav-collapse-toggle__icon" aria-hidden="true">${icons.panelRightOpen}</span>
|
||||
</button>
|
||||
@@ -1052,7 +1070,315 @@ function renderWorkspaceFileRail(
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
const files = workspaceFiles.list?.files ?? [];
|
||||
const files = sessionWorkspace.list?.files ?? [];
|
||||
const modifiedFiles = files.filter((file) => file.kind === "modified");
|
||||
const readFiles = files.filter((file) => file.kind === "read");
|
||||
const artifacts = sessionWorkspace.list?.artifacts ?? [];
|
||||
const browser = sessionWorkspace.list?.browser ?? null;
|
||||
const hasSessionItems = files.length > 0 || artifacts.length > 0;
|
||||
const hasBrowserItems = (browser?.entries.length ?? 0) > 0;
|
||||
const hasItems = hasSessionItems || hasBrowserItems;
|
||||
const renderPathActions = (
|
||||
path: string,
|
||||
options: { preview?: boolean } = {},
|
||||
): TemplateResult => html`
|
||||
<span
|
||||
class="chat-workspace-rail__row-actions"
|
||||
role="group"
|
||||
aria-label=${t("chat.workspaceFiles.actions")}
|
||||
>
|
||||
${options.preview === false
|
||||
? nothing
|
||||
: html`<button
|
||||
class="chat-workspace-rail__row-action"
|
||||
type="button"
|
||||
title=${t("chat.workspaceFiles.preview")}
|
||||
aria-label=${t("chat.workspaceFiles.preview")}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
sessionWorkspace.onOpenFile(path);
|
||||
}}
|
||||
>
|
||||
${icons.eye}
|
||||
</button>`}
|
||||
<button
|
||||
class="chat-workspace-rail__row-action"
|
||||
type="button"
|
||||
title=${t("chat.workspaceFiles.copyPath")}
|
||||
aria-label=${t("chat.workspaceFiles.copyPath")}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
sessionWorkspace.onCopyPath(path);
|
||||
}}
|
||||
>
|
||||
${icons.copy}
|
||||
</button>
|
||||
</span>
|
||||
`;
|
||||
const renderSessionSummary = (): TemplateResult | typeof nothing => {
|
||||
if (!sessionWorkspace.list) {
|
||||
return nothing;
|
||||
}
|
||||
const browserCount = browser?.entries.length ?? 0;
|
||||
return html`
|
||||
<div class="chat-workspace-rail__summary" aria-label=${t("chat.workspaceFiles.summary")}>
|
||||
<span
|
||||
>${t("chat.workspaceFiles.changedCount", { count: String(modifiedFiles.length) })}</span
|
||||
>
|
||||
<span>${t("chat.workspaceFiles.readCount", { count: String(readFiles.length) })}</span>
|
||||
<span>${t("chat.workspaceFiles.artifactCount", { count: String(artifacts.length) })}</span>
|
||||
<span>${t("chat.workspaceFiles.browserCount", { count: String(browserCount) })}</span>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
const renderFileRows = (rows: typeof files): TemplateResult | typeof nothing =>
|
||||
rows.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="chat-workspace-rail__list" role="list">
|
||||
${rows.map((file) => {
|
||||
const size = formatWorkspaceFileSize(file);
|
||||
const itemId = `file:${file.path}`;
|
||||
const isActive = itemId === sessionWorkspace.activeId;
|
||||
return html`
|
||||
<div
|
||||
class="chat-workspace-rail__file ${isActive
|
||||
? "chat-workspace-rail__file--active"
|
||||
: ""}"
|
||||
role="listitem"
|
||||
title=${file.path || file.name}
|
||||
>
|
||||
<button
|
||||
class="chat-workspace-rail__file-open"
|
||||
type="button"
|
||||
@click=${() => sessionWorkspace.onOpenFile(file.path)}
|
||||
>
|
||||
<span class="chat-workspace-rail__file-icon">${icons.fileText}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">${file.path || file.name}</span>
|
||||
${size
|
||||
? html`<span class="chat-workspace-rail__file-meta">${size}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
</button>
|
||||
${file.missing
|
||||
? html`<span class="chat-workspace-rail__file-badge"
|
||||
>${t("chat.workspaceFiles.missing")}</span
|
||||
>`
|
||||
: nothing}
|
||||
${renderPathActions(file.path)}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
const renderBrowserBadge = (
|
||||
sessionKind: "modified" | "read" | "mixed" | undefined,
|
||||
): TemplateResult | typeof nothing => {
|
||||
if (!sessionKind) {
|
||||
return nothing;
|
||||
}
|
||||
const label =
|
||||
sessionKind === "modified"
|
||||
? t("chat.workspaceFiles.changed")
|
||||
: sessionKind === "read"
|
||||
? t("chat.workspaceFiles.read")
|
||||
: t("chat.workspaceFiles.session");
|
||||
return html`<span class="chat-workspace-rail__file-badge">${label}</span>`;
|
||||
};
|
||||
const renderBrowserBreadcrumbs = (): TemplateResult | typeof nothing => {
|
||||
if (!browser || browser.search) {
|
||||
return nothing;
|
||||
}
|
||||
const parts = browser.path ? browser.path.split("/").filter(Boolean) : [];
|
||||
let currentPath = "";
|
||||
return html`
|
||||
<div class="chat-workspace-rail__breadcrumbs" aria-label=${t("chat.workspaceFiles.path")}>
|
||||
<button
|
||||
class="chat-workspace-rail__crumb"
|
||||
type="button"
|
||||
@click=${() => sessionWorkspace.onBrowsePath("")}
|
||||
>
|
||||
${t("chat.workspaceFiles.root")}
|
||||
</button>
|
||||
${parts.map((part) => {
|
||||
currentPath = currentPath ? `${currentPath}/${part}` : part;
|
||||
const pathForPart = currentPath;
|
||||
return html`
|
||||
<span class="chat-workspace-rail__crumb-separator">/</span>
|
||||
<button
|
||||
class="chat-workspace-rail__crumb"
|
||||
type="button"
|
||||
@click=${() => sessionWorkspace.onBrowsePath(pathForPart)}
|
||||
>
|
||||
${part}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
const renderBrowserRows = (): TemplateResult => {
|
||||
const entries = browser?.entries ?? [];
|
||||
const parentPath = browser?.parentPath;
|
||||
return html`
|
||||
<section class="chat-workspace-rail__browser">
|
||||
<div class="chat-workspace-rail__browser-tools">
|
||||
<label class="chat-workspace-rail__search">
|
||||
<span class="chat-workspace-rail__search-icon" aria-hidden="true">${icons.search}</span>
|
||||
<input
|
||||
type="search"
|
||||
placeholder=${t("chat.workspaceFiles.search")}
|
||||
aria-label=${t("chat.workspaceFiles.search")}
|
||||
.value=${browser?.search ?? ""}
|
||||
@input=${(event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
sessionWorkspace.onSearch(target.value);
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
${renderBrowserBreadcrumbs()}
|
||||
${browser?.search
|
||||
? html`<div class="chat-workspace-rail__browser-caption">
|
||||
${t("chat.workspaceFiles.searchResults")}
|
||||
</div>`
|
||||
: nothing}
|
||||
<div class="chat-workspace-rail__list chat-workspace-rail__list--browser" role="list">
|
||||
${!browser?.search && parentPath != null
|
||||
? html`
|
||||
<div
|
||||
class="chat-workspace-rail__file chat-workspace-rail__file--directory"
|
||||
role="listitem"
|
||||
>
|
||||
<button
|
||||
class="chat-workspace-rail__file-open"
|
||||
type="button"
|
||||
@click=${() => sessionWorkspace.onBrowsePath(parentPath)}
|
||||
>
|
||||
<span class="chat-workspace-rail__file-icon">${icons.folder}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">..</span>
|
||||
<span class="chat-workspace-rail__file-meta"
|
||||
>${t("chat.workspaceFiles.parentFolder")}</span
|
||||
>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
${entries.length === 0
|
||||
? html`<div class="chat-workspace-rail__state">
|
||||
${browser?.search
|
||||
? t("chat.workspaceFiles.noSearchResults")
|
||||
: t("chat.workspaceFiles.noBrowserFiles")}
|
||||
</div>`
|
||||
: entries.map((entry) => {
|
||||
const size = entry.kind === "file" ? formatWorkspaceFileSize(entry) : "";
|
||||
const itemId = `file:${entry.path}`;
|
||||
const isActive = itemId === sessionWorkspace.activeId;
|
||||
const canPreview = entry.kind === "file" && Boolean(entry.sessionKind);
|
||||
return html`
|
||||
<div
|
||||
class="chat-workspace-rail__file ${entry.kind === "directory"
|
||||
? "chat-workspace-rail__file--directory"
|
||||
: ""} ${isActive ? "chat-workspace-rail__file--active" : ""}"
|
||||
role="listitem"
|
||||
title=${entry.path || entry.name}
|
||||
>
|
||||
<button
|
||||
class="chat-workspace-rail__file-open"
|
||||
type="button"
|
||||
?disabled=${entry.kind === "file" && !canPreview}
|
||||
@click=${() =>
|
||||
entry.kind === "directory"
|
||||
? sessionWorkspace.onBrowsePath(entry.path)
|
||||
: canPreview
|
||||
? sessionWorkspace.onOpenFile(entry.path)
|
||||
: undefined}
|
||||
>
|
||||
<span class="chat-workspace-rail__file-icon"
|
||||
>${entry.kind === "directory" ? icons.folder : icons.fileText}</span
|
||||
>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">${entry.name}</span>
|
||||
<span class="chat-workspace-rail__file-meta">
|
||||
${entry.kind === "directory"
|
||||
? entry.path || t("chat.workspaceFiles.root")
|
||||
: [entry.path, size].filter(Boolean).join(" / ")}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
${renderBrowserBadge(entry.sessionKind)}
|
||||
${entry.kind === "file"
|
||||
? renderPathActions(entry.path, { preview: canPreview })
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
${browser?.truncated
|
||||
? html`<div class="chat-workspace-rail__state">
|
||||
${t("chat.workspaceFiles.truncated")}
|
||||
</div>`
|
||||
: nothing}
|
||||
</section>
|
||||
`;
|
||||
};
|
||||
const renderArtifactRows = (): TemplateResult | typeof nothing =>
|
||||
artifacts.length === 0
|
||||
? nothing
|
||||
: html`
|
||||
<div class="chat-workspace-rail__list" role="list">
|
||||
${artifacts.map((artifact) => {
|
||||
const size = renderWorkspaceArtifactSize(artifact);
|
||||
const itemId = `artifact:${artifact.id}`;
|
||||
const isActive = itemId === sessionWorkspace.activeId;
|
||||
const isImage = artifact.mimeType?.startsWith("image/");
|
||||
return html`
|
||||
<div
|
||||
class="chat-workspace-rail__file ${isActive
|
||||
? "chat-workspace-rail__file--active"
|
||||
: ""}"
|
||||
role="listitem"
|
||||
title=${artifact.title}
|
||||
>
|
||||
<button
|
||||
class="chat-workspace-rail__file-open"
|
||||
type="button"
|
||||
@click=${() => sessionWorkspace.onOpenArtifact(artifact.id)}
|
||||
>
|
||||
<span class="chat-workspace-rail__file-icon"
|
||||
>${isImage ? icons.image : icons.paperclip}</span
|
||||
>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">${artifact.title}</span>
|
||||
${size || artifact.mimeType
|
||||
? html`<span class="chat-workspace-rail__file-meta"
|
||||
>${[artifact.mimeType, size].filter(Boolean).join(" / ")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</span>
|
||||
</button>
|
||||
<span class="chat-workspace-rail__row-actions">
|
||||
<button
|
||||
class="chat-workspace-rail__row-action"
|
||||
type="button"
|
||||
title=${t("chat.workspaceFiles.preview")}
|
||||
aria-label=${t("chat.workspaceFiles.preview")}
|
||||
@click=${(event: Event) => {
|
||||
event.stopPropagation();
|
||||
sessionWorkspace.onOpenArtifact(artifact.id);
|
||||
}}
|
||||
>
|
||||
${icons.eye}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`;
|
||||
return html`
|
||||
<aside class="chat-workspace-rail" aria-label=${t("chat.workspaceFiles.label")}>
|
||||
<div class="chat-workspace-rail__header">
|
||||
@@ -1066,8 +1392,8 @@ function renderWorkspaceFileRail(
|
||||
type="button"
|
||||
title=${t("chat.workspaceFiles.refresh")}
|
||||
aria-label=${t("chat.workspaceFiles.refresh")}
|
||||
?disabled=${workspaceFiles.loading}
|
||||
@click=${workspaceFiles.onRefresh}
|
||||
?disabled=${sessionWorkspace.loading}
|
||||
@click=${sessionWorkspace.onRefresh}
|
||||
>
|
||||
${icons.refresh}
|
||||
</button>
|
||||
@@ -1077,7 +1403,7 @@ function renderWorkspaceFileRail(
|
||||
title=${t("chat.workspaceFiles.collapse")}
|
||||
aria-label=${t("chat.workspaceFiles.collapse")}
|
||||
aria-expanded="true"
|
||||
@click=${workspaceFiles.onToggleCollapsed}
|
||||
@click=${sessionWorkspace.onToggleCollapsed}
|
||||
>
|
||||
<span class="nav-collapse-toggle__icon" aria-hidden="true"
|
||||
>${icons.panelRightClose}</span
|
||||
@@ -1085,51 +1411,44 @@ function renderWorkspaceFileRail(
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
${workspaceFiles.list?.workspace
|
||||
? html`<div class="chat-workspace-rail__path" title=${workspaceFiles.list.workspace}>
|
||||
${workspaceFiles.list.workspace}
|
||||
${sessionWorkspace.list?.root
|
||||
? html`<div class="chat-workspace-rail__path" title=${sessionWorkspace.list.root}>
|
||||
${sessionWorkspace.list.root}
|
||||
</div>`
|
||||
: nothing}
|
||||
${workspaceFiles.error
|
||||
${renderSessionSummary()}
|
||||
${sessionWorkspace.error
|
||||
? html`<div class="chat-workspace-rail__state chat-workspace-rail__state--error">
|
||||
${workspaceFiles.error}
|
||||
${sessionWorkspace.error}
|
||||
</div>`
|
||||
: workspaceFiles.loading && files.length === 0
|
||||
: sessionWorkspace.loading && !hasItems
|
||||
? html`<div class="chat-workspace-rail__state">${t("chat.workspaceFiles.loading")}</div>`
|
||||
: files.length === 0
|
||||
? html`<div class="chat-workspace-rail__state">${t("chat.workspaceFiles.empty")}</div>`
|
||||
: html`
|
||||
<div class="chat-workspace-rail__list" role="list">
|
||||
${files.map((file) => {
|
||||
const size = formatWorkspaceFileSize(file);
|
||||
const isActive = file.name === workspaceFiles.activeName;
|
||||
return html`
|
||||
<button
|
||||
class="chat-workspace-rail__file ${isActive
|
||||
? "chat-workspace-rail__file--active"
|
||||
: ""}"
|
||||
type="button"
|
||||
role="listitem"
|
||||
title=${file.path || file.name}
|
||||
@click=${() => workspaceFiles.onOpenFile(file.name)}
|
||||
>
|
||||
<span class="chat-workspace-rail__file-icon">${icons.fileText}</span>
|
||||
<span class="chat-workspace-rail__file-main">
|
||||
<span class="chat-workspace-rail__file-name">${file.name}</span>
|
||||
${size
|
||||
? html`<span class="chat-workspace-rail__file-meta">${size}</span>`
|
||||
: nothing}
|
||||
</span>
|
||||
${file.missing
|
||||
? html`<span class="chat-workspace-rail__file-badge"
|
||||
>${t("chat.workspaceFiles.missing")}</span
|
||||
>`
|
||||
: nothing}
|
||||
</button>
|
||||
`;
|
||||
})}
|
||||
</div>
|
||||
`}
|
||||
: html`
|
||||
<div class="chat-workspace-rail__scroll">
|
||||
${!hasSessionItems
|
||||
? html`<div class="chat-workspace-rail__state">
|
||||
${t("chat.workspaceFiles.empty")}
|
||||
</div>`
|
||||
: html`
|
||||
${renderWorkspaceRailSection(
|
||||
t("chat.workspaceFiles.changed"),
|
||||
renderFileRows(modifiedFiles),
|
||||
)}
|
||||
${renderWorkspaceRailSection(
|
||||
t("chat.workspaceFiles.read"),
|
||||
renderFileRows(readFiles),
|
||||
)}
|
||||
${renderWorkspaceRailSection(
|
||||
t("chat.workspaceFiles.artifacts"),
|
||||
renderArtifactRows(),
|
||||
)}
|
||||
`}
|
||||
${renderWorkspaceRailSection(
|
||||
t("chat.workspaceFiles.browser"),
|
||||
browser ? renderBrowserRows() : nothing,
|
||||
)}
|
||||
</div>
|
||||
`}
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
@@ -2055,6 +2374,176 @@ export function renderChat(props: ChatProps) {
|
||||
const slashMenuVisible = isSlashMenuVisible();
|
||||
const activeSlashMenuOptionId = getActiveSlashMenuOptionId();
|
||||
const activeSlashMenuOptionLabel = getActiveSlashMenuOptionLabel();
|
||||
const chatColumnFooter = html`
|
||||
${renderChatQueue({
|
||||
queue: props.queue,
|
||||
canAbort: showAbortableUi,
|
||||
onQueueRetry: props.onQueueRetry,
|
||||
onQueueSteer: props.onQueueSteer,
|
||||
onQueueRemove: props.onQueueRemove,
|
||||
})}
|
||||
${renderSideResult(props.sideResult, props.onDismissSideResult)}
|
||||
${props.showNewMessages
|
||||
? html`
|
||||
<button class="chat-new-messages" type="button" @click=${props.onScrollToBottom}>
|
||||
${icons.arrowDown} New messages
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<!-- Input bar -->
|
||||
<div
|
||||
class="agent-chat__input"
|
||||
@click=${(event: MouseEvent) => focusComposerFromChrome(event, props.connected)}
|
||||
>
|
||||
${renderSlashMenu(requestUpdate, props, visibleDraft)} ${renderAttachmentPreview(props)}
|
||||
<div class="agent-chat__composer-status-stack">
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderContextNotice(activeSession, props.sessions?.defaults?.contextTokens ?? null, {
|
||||
compactBusy,
|
||||
compactDisabled: !props.connected || isBusy || showAbortableUi,
|
||||
onCompact: props.onCompact,
|
||||
})}
|
||||
${renderChatGoal(activeSession?.goal)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept=${CHAT_ATTACHMENT_ACCEPT}
|
||||
multiple
|
||||
class="agent-chat__file-input"
|
||||
@change=${(e: Event) => handleFileSelect(e, props)}
|
||||
/>
|
||||
|
||||
${renderRealtimeTalkOptions(props)}
|
||||
${props.realtimeTalkActive || props.realtimeTalkDetail || props.realtimeTalkTranscript
|
||||
? html`
|
||||
<div class="agent-chat__stt-interim agent-chat__talk-status">
|
||||
${props.realtimeTalkDetail ??
|
||||
((props.realtimeTalkConversation?.length ?? 0) === 0
|
||||
? props.realtimeTalkTranscript
|
||||
: null) ??
|
||||
(props.realtimeTalkStatus === "thinking"
|
||||
? "Asking OpenClaw..."
|
||||
: props.realtimeTalkStatus === "connecting"
|
||||
? "Connecting Talk..."
|
||||
: "Talk live")}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea
|
||||
${ref((el) => {
|
||||
composerTextarea = el instanceof HTMLTextAreaElement ? el : null;
|
||||
if (composerTextarea) {
|
||||
adjustTextareaHeight(composerTextarea);
|
||||
}
|
||||
})}
|
||||
.value=${visibleDraft}
|
||||
dir=${detectTextDirection(visibleDraft)}
|
||||
?disabled=${!props.connected}
|
||||
aria-autocomplete="list"
|
||||
aria-controls=${ifDefined(slashMenuVisible ? SLASH_MENU_LISTBOX_ID : undefined)}
|
||||
aria-activedescendant=${ifDefined(activeSlashMenuOptionId ?? undefined)}
|
||||
aria-describedby=${SLASH_MENU_ACTIVE_ANNOUNCEMENT_ID}
|
||||
@keydown=${handleKeyDown}
|
||||
@input=${handleInput}
|
||||
@blur=${handleBlur}
|
||||
@paste=${(e: ClipboardEvent) => handlePaste(e, props)}
|
||||
placeholder=${placeholder}
|
||||
rows="1"
|
||||
></textarea>
|
||||
<span
|
||||
id=${SLASH_MENU_ACTIVE_ANNOUNCEMENT_ID}
|
||||
class="agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${activeSlashMenuOptionLabel}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__toolbar">
|
||||
<div class="agent-chat__toolbar-left">
|
||||
<button
|
||||
type="button"
|
||||
class="agent-chat__input-btn"
|
||||
@click=${clickComposerFileInput}
|
||||
title=${t("chat.composer.attachFile")}
|
||||
aria-label=${t("chat.composer.attachFile")}
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${icons.paperclip}
|
||||
<span class="agent-chat__control-label">${t("chat.composer.attachFile")}</span>
|
||||
</button>
|
||||
|
||||
${props.onToggleRealtimeTalk
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${props.realtimeTalkActive
|
||||
? "agent-chat__input-btn--talk"
|
||||
: ""}"
|
||||
@click=${props.onToggleRealtimeTalk}
|
||||
title=${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}
|
||||
aria-label=${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${props.realtimeTalkActive ? icons.volume2 : icons.radio}
|
||||
<span class="agent-chat__control-label"
|
||||
>${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}</span
|
||||
>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${props.onToggleRealtimeTalkOptions
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${props.realtimeTalkOptionsOpen
|
||||
? "agent-chat__input-btn--talk"
|
||||
: ""}"
|
||||
@click=${props.onToggleRealtimeTalkOptions}
|
||||
title="Talk settings"
|
||||
aria-label="Talk settings"
|
||||
aria-expanded=${props.realtimeTalkOptionsOpen ? "true" : "false"}
|
||||
?disabled=${!props.connected || props.realtimeTalkActive}
|
||||
>
|
||||
${icons.settings}
|
||||
<span class="agent-chat__control-label">Talk settings</span>
|
||||
</button>
|
||||
`
|
||||
: nothing}
|
||||
${tokens ? html`<span class="agent-chat__token-count">${tokens}</span>` : nothing}
|
||||
${renderChatRunStatusIndicator(composerRunStatus)}
|
||||
</div>
|
||||
|
||||
${composerControls && composerControls !== nothing
|
||||
? html`<div class="agent-chat__composer-controls">${composerControls}</div>`
|
||||
: nothing}
|
||||
${renderChatRunControls({
|
||||
canAbort: showAbortableUi,
|
||||
connected: props.connected,
|
||||
draft: visibleDraft,
|
||||
hasMessages: props.messages.length > 0,
|
||||
isBusy,
|
||||
sending: props.sending,
|
||||
onAbort: props.onAbort,
|
||||
onExport: () => exportMarkdown(props),
|
||||
onNewSession: props.onNewSession,
|
||||
onSend: handleSend,
|
||||
onStoreDraft: () => {},
|
||||
showSecondary: false,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return html`
|
||||
<section
|
||||
@@ -2104,17 +2593,17 @@ export function renderChat(props: ChatProps) {
|
||||
|
||||
<div
|
||||
class="chat-workbench ${
|
||||
props.workspaceFiles?.collapsed ? "chat-workbench--workspace-collapsed" : ""
|
||||
props.sessionWorkspace?.collapsed ? "chat-workbench--workspace-collapsed" : ""
|
||||
}"
|
||||
>
|
||||
${renderWorkspaceFileRail(props.workspaceFiles)}
|
||||
${renderSessionWorkspaceRail(props.sessionWorkspace)}
|
||||
<div class="chat-workbench__main">
|
||||
<div class="chat-split-container ${sidebarOpen ? "chat-split-container--open" : ""}">
|
||||
<div
|
||||
class="chat-main"
|
||||
style="flex: ${sidebarOpen ? `0 0 ${splitRatio * 100}%` : "1 1 100%"}"
|
||||
style="flex: ${sidebarOpen ? `0 1 ${splitRatio * 100}%` : "1 1 100%"}"
|
||||
>
|
||||
${thread}
|
||||
${thread} ${chatColumnFooter}
|
||||
</div>
|
||||
|
||||
${
|
||||
@@ -2149,184 +2638,6 @@ export function renderChat(props: ChatProps) {
|
||||
}
|
||||
</div>
|
||||
|
||||
${renderChatQueue({
|
||||
queue: props.queue,
|
||||
canAbort: showAbortableUi,
|
||||
onQueueRetry: props.onQueueRetry,
|
||||
onQueueSteer: props.onQueueSteer,
|
||||
onQueueRemove: props.onQueueRemove,
|
||||
})}
|
||||
${renderSideResult(props.sideResult, props.onDismissSideResult)}
|
||||
${
|
||||
props.showNewMessages
|
||||
? html`
|
||||
<button class="chat-new-messages" type="button" @click=${props.onScrollToBottom}>
|
||||
${icons.arrowDown} New messages
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<!-- Input bar -->
|
||||
<div
|
||||
class="agent-chat__input"
|
||||
@click=${(event: MouseEvent) => focusComposerFromChrome(event, props.connected)}
|
||||
>
|
||||
${renderSlashMenu(requestUpdate, props, visibleDraft)} ${renderAttachmentPreview(props)}
|
||||
<div class="agent-chat__composer-status-stack">
|
||||
${renderFallbackIndicator(props.fallbackStatus)}
|
||||
${renderCompactionIndicator(props.compactionStatus)}
|
||||
${renderContextNotice(activeSession, props.sessions?.defaults?.contextTokens ?? null, {
|
||||
compactBusy,
|
||||
compactDisabled: !props.connected || isBusy || showAbortableUi,
|
||||
onCompact: props.onCompact,
|
||||
})}
|
||||
${renderChatGoal(activeSession?.goal)}
|
||||
</div>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
accept=${CHAT_ATTACHMENT_ACCEPT}
|
||||
multiple
|
||||
class="agent-chat__file-input"
|
||||
@change=${(e: Event) => handleFileSelect(e, props)}
|
||||
/>
|
||||
|
||||
${renderRealtimeTalkOptions(props)}
|
||||
${
|
||||
props.realtimeTalkActive || props.realtimeTalkDetail || props.realtimeTalkTranscript
|
||||
? html`
|
||||
<div class="agent-chat__stt-interim agent-chat__talk-status">
|
||||
${props.realtimeTalkDetail ??
|
||||
((props.realtimeTalkConversation?.length ?? 0) === 0
|
||||
? props.realtimeTalkTranscript
|
||||
: null) ??
|
||||
(props.realtimeTalkStatus === "thinking"
|
||||
? "Asking OpenClaw..."
|
||||
: props.realtimeTalkStatus === "connecting"
|
||||
? "Connecting Talk..."
|
||||
: "Talk live")}
|
||||
</div>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
|
||||
<div class="agent-chat__composer-combobox">
|
||||
<textarea
|
||||
${ref((el) => {
|
||||
composerTextarea = el instanceof HTMLTextAreaElement ? el : null;
|
||||
if (composerTextarea) {
|
||||
adjustTextareaHeight(composerTextarea);
|
||||
}
|
||||
})}
|
||||
.value=${visibleDraft}
|
||||
dir=${detectTextDirection(visibleDraft)}
|
||||
?disabled=${!props.connected}
|
||||
aria-autocomplete="list"
|
||||
aria-controls=${ifDefined(slashMenuVisible ? SLASH_MENU_LISTBOX_ID : undefined)}
|
||||
aria-activedescendant=${ifDefined(activeSlashMenuOptionId ?? undefined)}
|
||||
aria-describedby=${SLASH_MENU_ACTIVE_ANNOUNCEMENT_ID}
|
||||
@keydown=${handleKeyDown}
|
||||
@input=${handleInput}
|
||||
@blur=${handleBlur}
|
||||
@paste=${(e: ClipboardEvent) => handlePaste(e, props)}
|
||||
placeholder=${placeholder}
|
||||
rows="1"
|
||||
></textarea>
|
||||
<span
|
||||
id=${SLASH_MENU_ACTIVE_ANNOUNCEMENT_ID}
|
||||
class="agent-chat__sr-only"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>${activeSlashMenuOptionLabel}</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="agent-chat__toolbar">
|
||||
<div class="agent-chat__toolbar-left">
|
||||
<button
|
||||
type="button"
|
||||
class="agent-chat__input-btn"
|
||||
@click=${clickComposerFileInput}
|
||||
title=${t("chat.composer.attachFile")}
|
||||
aria-label=${t("chat.composer.attachFile")}
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${icons.paperclip}
|
||||
<span class="agent-chat__control-label">${t("chat.composer.attachFile")}</span>
|
||||
</button>
|
||||
|
||||
${
|
||||
props.onToggleRealtimeTalk
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${props.realtimeTalkActive
|
||||
? "agent-chat__input-btn--talk"
|
||||
: ""}"
|
||||
@click=${props.onToggleRealtimeTalk}
|
||||
title=${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}
|
||||
aria-label=${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}
|
||||
?disabled=${!props.connected}
|
||||
>
|
||||
${props.realtimeTalkActive ? icons.volume2 : icons.radio}
|
||||
<span class="agent-chat__control-label"
|
||||
>${props.realtimeTalkActive
|
||||
? t("chat.composer.stopTalk")
|
||||
: t("chat.composer.startTalk")}</span
|
||||
>
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
props.onToggleRealtimeTalkOptions
|
||||
? html`
|
||||
<button
|
||||
class="agent-chat__input-btn ${props.realtimeTalkOptionsOpen
|
||||
? "agent-chat__input-btn--talk"
|
||||
: ""}"
|
||||
@click=${props.onToggleRealtimeTalkOptions}
|
||||
title="Talk settings"
|
||||
aria-label="Talk settings"
|
||||
aria-expanded=${props.realtimeTalkOptionsOpen ? "true" : "false"}
|
||||
?disabled=${!props.connected || props.realtimeTalkActive}
|
||||
>
|
||||
${icons.settings}
|
||||
<span class="agent-chat__control-label">Talk settings</span>
|
||||
</button>
|
||||
`
|
||||
: nothing
|
||||
}
|
||||
${tokens ? html`<span class="agent-chat__token-count">${tokens}</span>` : nothing}
|
||||
${renderChatRunStatusIndicator(composerRunStatus)}
|
||||
</div>
|
||||
|
||||
${
|
||||
composerControls && composerControls !== nothing
|
||||
? html`<div class="agent-chat__composer-controls">${composerControls}</div>`
|
||||
: nothing
|
||||
}
|
||||
${renderChatRunControls({
|
||||
canAbort: showAbortableUi,
|
||||
connected: props.connected,
|
||||
draft: visibleDraft,
|
||||
hasMessages: props.messages.length > 0,
|
||||
isBusy,
|
||||
sending: props.sending,
|
||||
onAbort: props.onAbort,
|
||||
onExport: () => exportMarkdown(props),
|
||||
onNewSession: props.onNewSession,
|
||||
onSend: handleSend,
|
||||
onStoreDraft: () => {},
|
||||
showSecondary: false,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
`;
|
||||
|
||||
@@ -43,16 +43,18 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
props.allowExternalEmbedUrls ?? false,
|
||||
)
|
||||
: null;
|
||||
const title =
|
||||
content?.kind === "canvas"
|
||||
? content.title?.trim() || "Render Preview"
|
||||
: content?.kind === "image"
|
||||
? content.title.trim() || "Image Preview"
|
||||
: content?.kind === "markdown"
|
||||
? "Markdown Preview"
|
||||
: "Tool Details";
|
||||
return html`
|
||||
<div class="sidebar-panel">
|
||||
<div class="sidebar-header">
|
||||
<div class="sidebar-title">
|
||||
${content?.kind === "canvas"
|
||||
? content.title?.trim() || "Render Preview"
|
||||
: content?.kind === "markdown"
|
||||
? "Markdown Preview"
|
||||
: "Tool Details"}
|
||||
</div>
|
||||
<div class="sidebar-title">${title}</div>
|
||||
<button
|
||||
@click=${props.onClose}
|
||||
class="btn"
|
||||
@@ -111,33 +113,57 @@ export function renderMarkdownSidebar(props: MarkdownSidebarProps) {
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<section class="sidebar-markdown-shell">
|
||||
<div class="sidebar-markdown-shell__toolbar">
|
||||
<div class="sidebar-markdown-shell__intro">
|
||||
<div class="sidebar-markdown-shell__eyebrow">
|
||||
${icons.scrollText}
|
||||
<span>Rendered Markdown</span>
|
||||
</div>
|
||||
<div class="sidebar-markdown-shell__hint">
|
||||
Sanitized rich-text preview for quick reading.
|
||||
</div>
|
||||
: content.kind === "image"
|
||||
? html`
|
||||
<div class="chat-tool-card__preview" data-kind="image">
|
||||
<div class="chat-tool-card__preview-panel" data-side="front">
|
||||
<img
|
||||
class="chat-tool-card__preview-image"
|
||||
src=${content.src}
|
||||
alt=${title}
|
||||
style="display:block;max-width:100%;height:auto;border-radius:8px;"
|
||||
/>
|
||||
</div>
|
||||
<button @click=${props.onViewRawText} class="btn btn--sm" type="button">
|
||||
View Raw Text
|
||||
</button>
|
||||
${content.rawText?.trim()
|
||||
? html`
|
||||
<div style="margin-top: 12px;">
|
||||
<button @click=${props.onViewRawText} class="btn" type="button">
|
||||
View Raw Text
|
||||
</button>
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
${markdownHtml
|
||||
? html`
|
||||
<article class="sidebar-markdown-reader sidebar-markdown">
|
||||
${unsafeHTML(markdownHtml)}
|
||||
</article>
|
||||
`
|
||||
: html`
|
||||
<div class="sidebar-markdown-empty">No previewable markdown content.</div>
|
||||
`}
|
||||
</section>
|
||||
`
|
||||
`
|
||||
: html`
|
||||
<section class="sidebar-markdown-shell">
|
||||
<div class="sidebar-markdown-shell__toolbar">
|
||||
<div class="sidebar-markdown-shell__intro">
|
||||
<div class="sidebar-markdown-shell__eyebrow">
|
||||
${icons.scrollText}
|
||||
<span>Rendered Markdown</span>
|
||||
</div>
|
||||
<div class="sidebar-markdown-shell__hint">
|
||||
Sanitized rich-text preview for quick reading.
|
||||
</div>
|
||||
</div>
|
||||
<button @click=${props.onViewRawText} class="btn btn--sm" type="button">
|
||||
View Raw Text
|
||||
</button>
|
||||
</div>
|
||||
${markdownHtml
|
||||
? html`
|
||||
<article class="sidebar-markdown-reader sidebar-markdown">
|
||||
${unsafeHTML(markdownHtml)}
|
||||
</article>
|
||||
`
|
||||
: html`
|
||||
<div class="sidebar-markdown-empty">
|
||||
No previewable markdown content.
|
||||
</div>
|
||||
`}
|
||||
</section>
|
||||
`
|
||||
: html` <div class="muted">No content available</div> `}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user