nolo-cli 0.1.21 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/agent-runtime/agentRecordConfig.ts +4 -0
  2. package/agent-runtime/hostAdapter.ts +2 -0
  3. package/agent-runtime/index.ts +7 -0
  4. package/agent-runtime/localLoop.ts +2 -0
  5. package/agent-runtime/platformChatProvider.ts +3 -0
  6. package/agent-runtime/runtimeToolPolicy.ts +92 -0
  7. package/agent-runtime/types.ts +42 -0
  8. package/agentRunCommand.ts +74 -1
  9. package/agentRuntimeCommands.ts +17 -89
  10. package/ai/agent/streamAgentChatTurn.ts +104 -20
  11. package/ai/chat/fetchUtils.native.ts +2 -0
  12. package/ai/chat/fetchUtils.ts +2 -0
  13. package/ai/chat/sendOpenAICompletionsRequest.ts +56 -0
  14. package/ai/chat/sendOpenAIResponseRequest.ts +64 -0
  15. package/ai/llm/kimi.ts +1 -1
  16. package/ai/llm/providers.ts +3 -0
  17. package/ai/llm/reasoningModels.ts +1 -0
  18. package/ai/skills/skillDocProtocol.ts +95 -3
  19. package/ai/taskRun/taskRunProtocol.ts +1 -0
  20. package/ai/tools/agent/agentTools.ts +17 -0
  21. package/ai/tools/agent/startAgentDialogTool.ts +53 -0
  22. package/ai/tools/modelUsageTools.ts +5 -0
  23. package/client/agentRun.test.ts +257 -7
  24. package/client/agentRun.ts +133 -34
  25. package/client/localRuntimeAdapter.test.ts +2 -0
  26. package/client/localRuntimeAdapter.ts +15 -2
  27. package/database/actions/common.ts +4 -3
  28. package/database/config.ts +19 -0
  29. package/machineCommands.ts +400 -45
  30. package/package.json +4 -2
  31. package/render/canvas/canvasEditContext.ts +127 -0
  32. package/render/canvas/canvasRuntime.ts +57 -0
  33. package/render/canvas/canvasSnapshotParser.ts +76 -0
  34. package/render/canvas/canvasTree.ts +308 -0
  35. package/render/canvas/types.ts +46 -0
  36. package/render/layout/deleteBehavior.ts +52 -0
  37. package/render/layout/mainLayoutSidebar.ts +17 -0
  38. package/render/layout/mainLayoutViewMode.ts +56 -0
  39. package/render/layout/topbarUtils.ts +87 -0
  40. package/render/layout/useDevReloadPending.ts +30 -0
  41. package/render/page/createPageAction.ts +183 -0
  42. package/render/page/docSlice.ts +468 -0
  43. package/render/page/server/createPage.ts +174 -0
  44. package/render/page/server/handleCreatePage.ts +91 -0
  45. package/render/page/server/index.ts +4 -0
  46. package/render/page/types.ts +17 -0
  47. package/render/page/useKeyboardSave.ts +48 -0
  48. package/render/styles/zIndex.ts +12 -0
  49. package/render/surf/WeatherIconStyles.ts +17 -0
  50. package/render/surf/color.ts +9 -0
  51. package/render/surf/config.ts +46 -0
  52. package/render/surf/screens/style.ts +1 -0
  53. package/render/surf/styles/ToggleButtonStyles.ts +8 -0
  54. package/render/surf/utils/groupedWeatherData.ts +32 -0
  55. package/render/surf/weatherUtils.ts +50 -0
  56. package/render/table/activityColumns.ts +6 -0
  57. package/render/table/createTableAction.ts +270 -0
  58. package/render/table/deleteTableAction.ts +129 -0
  59. package/render/table/fetchAndCacheTableRows.ts +174 -0
  60. package/render/table/tableSlice.ts +1106 -0
  61. package/render/table/tableView.ts +289 -0
  62. package/render/table/toolValueUtils.ts +363 -0
  63. package/render/table/types.ts +252 -0
  64. package/render/table/useCreateTable.ts +72 -0
  65. package/render/table/useTable.ts +61 -0
  66. package/render/table/utils/tableSerialization.ts +50 -0
  67. package/render/web/elements/artifactPreviewCode.ts +43 -0
  68. package/render/web/elements/artifactRuntimePreload.ts +52 -0
  69. package/render/web/elements/codeBlockAutoPreview.ts +10 -0
  70. package/render/web/elements/mermaidPreview.ts +21 -0
  71. package/render/web/ui/useInlineEdit.ts +135 -0
  72. package/tableCommands.ts +42 -5
@@ -0,0 +1,57 @@
1
+ export type CanvasRuntimeAction =
2
+ | {
3
+ type: "toggle";
4
+ key: string;
5
+ }
6
+ | {
7
+ type: "setState";
8
+ key: string;
9
+ value: unknown;
10
+ }
11
+ | {
12
+ type: "scrollTo";
13
+ targetId: string;
14
+ };
15
+
16
+ export type CanvasRuntimeState = Record<string, unknown>;
17
+
18
+ export function parseCanvasRuntimeAction(action: unknown): CanvasRuntimeAction | null {
19
+ if (!action || typeof action !== "object") return null;
20
+ const candidate = action as Record<string, unknown>;
21
+ const type = candidate.type;
22
+
23
+ if (type === "toggle" && typeof candidate.key === "string" && candidate.key.trim()) {
24
+ return { type, key: candidate.key };
25
+ }
26
+
27
+ if (type === "setState" && typeof candidate.key === "string" && candidate.key.trim()) {
28
+ return { type, key: candidate.key, value: candidate.value };
29
+ }
30
+
31
+ if (type === "scrollTo" && typeof candidate.targetId === "string" && candidate.targetId.trim()) {
32
+ return { type, targetId: candidate.targetId };
33
+ }
34
+
35
+ return null;
36
+ }
37
+
38
+ export function reduceCanvasRuntimeAction(
39
+ state: CanvasRuntimeState,
40
+ action: CanvasRuntimeAction
41
+ ): CanvasRuntimeState {
42
+ if (action.type === "toggle") {
43
+ return {
44
+ ...state,
45
+ [action.key]: !state[action.key],
46
+ };
47
+ }
48
+
49
+ if (action.type === "setState") {
50
+ return {
51
+ ...state,
52
+ [action.key]: action.value,
53
+ };
54
+ }
55
+
56
+ return state;
57
+ }
@@ -0,0 +1,76 @@
1
+ import {
2
+ applyCanvasEvent,
3
+ createCanvasDocument,
4
+ normalizeCanvasEvent,
5
+ } from "./canvasTree";
6
+ import type { CanvasDocument, CanvasEvent } from "./types";
7
+
8
+ export type ParsedCanvasSnapshotMessage = {
9
+ document: CanvasDocument;
10
+ events: CanvasEvent[];
11
+ eventCount: number;
12
+ };
13
+
14
+ export function hasCanvasSnapshotSignal(content: string): boolean {
15
+ return content.includes("canvas_snapshot");
16
+ }
17
+
18
+ export function extractCanvasSnapshotText(content: unknown): string | null {
19
+ if (typeof content === "string") {
20
+ return hasCanvasSnapshotSignal(content) ? content : null;
21
+ }
22
+ if (!Array.isArray(content)) return null;
23
+
24
+ const text = content
25
+ .map((item) =>
26
+ item?.type === "text" && typeof item.text === "string" ? item.text : ""
27
+ )
28
+ .filter(Boolean)
29
+ .join("\n");
30
+
31
+ return hasCanvasSnapshotSignal(text) ? text : null;
32
+ }
33
+
34
+ function isCanvasEvent(value: unknown): value is CanvasEvent {
35
+ if (!value || typeof value !== "object") return false;
36
+ const event = value as { type?: unknown };
37
+ return (
38
+ event.type === "appendNode" ||
39
+ event.type === "updateNode" ||
40
+ event.type === "selectNode"
41
+ );
42
+ }
43
+
44
+ export function parseCanvasSnapshotMessage(
45
+ content: string
46
+ ): ParsedCanvasSnapshotMessage | null {
47
+ if (!hasCanvasSnapshotSignal(content)) return null;
48
+
49
+ let document = createCanvasDocument("root");
50
+ const events: CanvasEvent[] = [];
51
+ let eventCount = 0;
52
+
53
+ for (const line of content.split(/\r?\n/)) {
54
+ const trimmed = line.trim();
55
+ if (!trimmed.startsWith("{") || !trimmed.includes("canvas_snapshot")) {
56
+ continue;
57
+ }
58
+
59
+ try {
60
+ const parsed = JSON.parse(trimmed);
61
+ if (parsed?.type !== "canvas_snapshot" || !isCanvasEvent(parsed.event)) {
62
+ continue;
63
+ }
64
+ const event = normalizeCanvasEvent(parsed.event);
65
+ if (!event) continue;
66
+ document = applyCanvasEvent(document, event);
67
+ events.push(event);
68
+ eventCount += 1;
69
+ } catch {
70
+ // Streaming messages often end with a partial JSON line. Ignore it until
71
+ // the next render provides a complete line.
72
+ }
73
+ }
74
+
75
+ return eventCount > 0 ? { document, events, eventCount } : null;
76
+ }
@@ -0,0 +1,308 @@
1
+ import type { CanvasDocument, CanvasEvent, CanvasNode } from "./types";
2
+
3
+ const ALLOWED_NODE_TYPES = new Set([
4
+ "Canvas",
5
+ "Stack",
6
+ "Grid",
7
+ "Section",
8
+ "Text",
9
+ "Button",
10
+ "Card",
11
+ "MetricCard",
12
+ "Chart",
13
+ "List",
14
+ "Table",
15
+ "Toolbar",
16
+ ]);
17
+
18
+ const ALLOWED_STYLE_KEYS = new Set([
19
+ "alignItems",
20
+ "background",
21
+ "backgroundColor",
22
+ "border",
23
+ "borderColor",
24
+ "borderRadius",
25
+ "color",
26
+ "display",
27
+ "fontSize",
28
+ "fontWeight",
29
+ "gap",
30
+ "gridTemplateColumns",
31
+ "height",
32
+ "justifyContent",
33
+ "margin",
34
+ "maxWidth",
35
+ "minHeight",
36
+ "padding",
37
+ "textAlign",
38
+ "width",
39
+ ]);
40
+
41
+ export function createCanvasDocument(rootId = "root"): CanvasDocument {
42
+ return {
43
+ version: 1,
44
+ root: {
45
+ id: rootId,
46
+ type: "Canvas",
47
+ children: [],
48
+ },
49
+ selectedNodeId: null,
50
+ };
51
+ }
52
+
53
+ function isRecord(value: unknown): value is Record<string, unknown> {
54
+ return !!value && typeof value === "object" && !Array.isArray(value);
55
+ }
56
+
57
+ function isSafeId(value: unknown): value is string {
58
+ return typeof value === "string" && /^[a-zA-Z0-9_-]{1,80}$/.test(value);
59
+ }
60
+
61
+ function isSafePrimitive(value: unknown) {
62
+ return (
63
+ typeof value === "string" ||
64
+ typeof value === "number" ||
65
+ typeof value === "boolean"
66
+ );
67
+ }
68
+
69
+ function isSafeRuntimeValue(value: unknown) {
70
+ return (
71
+ isSafePrimitive(value) ||
72
+ (Array.isArray(value) && value.every((item) => isSafePrimitive(item)))
73
+ );
74
+ }
75
+
76
+ function sanitizeRuntimeAction(value: unknown) {
77
+ if (!isRecord(value) || typeof value.type !== "string") return null;
78
+
79
+ if (value.type === "toggle" && isSafeId(value.key)) {
80
+ return { type: "toggle", key: value.key };
81
+ }
82
+
83
+ if (
84
+ value.type === "setState" &&
85
+ isSafeId(value.key) &&
86
+ isSafeRuntimeValue(value.value)
87
+ ) {
88
+ return { type: "setState", key: value.key, value: value.value };
89
+ }
90
+
91
+ if (value.type === "scrollTo" && isSafeId(value.targetId)) {
92
+ return { type: "scrollTo", targetId: value.targetId };
93
+ }
94
+
95
+ return null;
96
+ }
97
+
98
+ function sanitizeProps(props: unknown): Record<string, unknown> | undefined {
99
+ if (!isRecord(props)) return undefined;
100
+
101
+ const safeProps: Record<string, unknown> = {};
102
+ for (const [key, value] of Object.entries(props)) {
103
+ if (key.startsWith("on") || key === "dangerouslySetInnerHTML") continue;
104
+
105
+ if (key === "action" || key === "runtimeAction") {
106
+ const action = sanitizeRuntimeAction(value);
107
+ if (action) safeProps[key] = action;
108
+ continue;
109
+ }
110
+
111
+ if (isSafePrimitive(value)) {
112
+ safeProps[key] = value;
113
+ continue;
114
+ }
115
+
116
+ if (
117
+ Array.isArray(value) &&
118
+ value.every((item) => isSafePrimitive(item))
119
+ ) {
120
+ safeProps[key] = value;
121
+ continue;
122
+ }
123
+
124
+ if (
125
+ key === "rows" &&
126
+ Array.isArray(value) &&
127
+ value.every((row) => Array.isArray(row) && row.every(isSafePrimitive))
128
+ ) {
129
+ safeProps[key] = value;
130
+ }
131
+ }
132
+
133
+ return Object.keys(safeProps).length ? safeProps : undefined;
134
+ }
135
+
136
+ function sanitizeStyle(style: unknown): Record<string, string | number> | undefined {
137
+ if (!isRecord(style)) return undefined;
138
+
139
+ const safeStyle: Record<string, string | number> = {};
140
+ for (const [key, value] of Object.entries(style)) {
141
+ if (!ALLOWED_STYLE_KEYS.has(key)) continue;
142
+ if (typeof value !== "string" && typeof value !== "number") continue;
143
+ if (typeof value === "string" && /url\s*\(|expression\s*\(/i.test(value)) continue;
144
+ safeStyle[key] = value;
145
+ }
146
+
147
+ return Object.keys(safeStyle).length ? safeStyle : undefined;
148
+ }
149
+
150
+ function sanitizeCanvasNode(node: unknown): CanvasNode | null {
151
+ if (!isRecord(node) || !isSafeId(node.id) || typeof node.type !== "string") {
152
+ return null;
153
+ }
154
+ if (!ALLOWED_NODE_TYPES.has(node.type)) return null;
155
+
156
+ const children = Array.isArray(node.children)
157
+ ? node.children
158
+ .map((child) => sanitizeCanvasNode(child))
159
+ .filter((child): child is CanvasNode => !!child)
160
+ : undefined;
161
+ const props = sanitizeProps(node.props);
162
+ const style = sanitizeStyle(node.style);
163
+
164
+ return {
165
+ id: node.id,
166
+ type: node.type as CanvasNode["type"],
167
+ ...(props ? { props } : {}),
168
+ ...(style ? { style } : {}),
169
+ ...(children?.length ? { children } : {}),
170
+ };
171
+ }
172
+
173
+ export function normalizeCanvasEvent(event: CanvasEvent): CanvasEvent | null {
174
+ if (event.type === "selectNode") {
175
+ return event.id === null || isSafeId(event.id) ? event : null;
176
+ }
177
+
178
+ if (event.type === "appendNode") {
179
+ if (!isSafeId(event.parentId)) return null;
180
+ const node = sanitizeCanvasNode(event.node);
181
+ return node ? { type: "appendNode", parentId: event.parentId, node } : null;
182
+ }
183
+
184
+ if (!isSafeId(event.id) || !isRecord(event.patch)) return null;
185
+ const props = sanitizeProps(event.patch.props);
186
+ const style = sanitizeStyle(event.patch.style);
187
+ if (!props && !style) return null;
188
+
189
+ return {
190
+ type: "updateNode",
191
+ id: event.id,
192
+ patch: {
193
+ ...(props ? { props } : {}),
194
+ ...(style ? { style } : {}),
195
+ },
196
+ };
197
+ }
198
+
199
+ export function getCanvasNodeSelectionPayload(document: CanvasDocument) {
200
+ if (!document.selectedNodeId) return null;
201
+ const path = findCanvasNodePath(document.root, document.selectedNodeId);
202
+ const selectedNode = path?.at(-1);
203
+ if (!path || !selectedNode) return null;
204
+
205
+ return {
206
+ selectedNodeId: selectedNode.id,
207
+ part: String(selectedNode.props?.part ?? selectedNode.id),
208
+ path: path.map((node) => node.id),
209
+ type: selectedNode.type,
210
+ props: selectedNode.props ?? {},
211
+ style: selectedNode.style ?? {},
212
+ };
213
+ }
214
+
215
+ export function findCanvasNodePath(
216
+ node: CanvasNode,
217
+ targetId: string,
218
+ path: CanvasNode[] = []
219
+ ): CanvasNode[] | null {
220
+ const nextPath = [...path, node];
221
+ if (node.id === targetId) return nextPath;
222
+
223
+ for (const child of node.children ?? []) {
224
+ const result = findCanvasNodePath(child, targetId, nextPath);
225
+ if (result) return result;
226
+ }
227
+
228
+ return null;
229
+ }
230
+
231
+ function updateNodeTree(
232
+ node: CanvasNode,
233
+ targetId: string,
234
+ updater: (node: CanvasNode) => CanvasNode
235
+ ): { node: CanvasNode; changed: boolean } {
236
+ if (node.id === targetId) {
237
+ return { node: updater(node), changed: true };
238
+ }
239
+
240
+ let changed = false;
241
+ const children = (node.children ?? []).map((child) => {
242
+ const result = updateNodeTree(child, targetId, updater);
243
+ changed ||= result.changed;
244
+ return result.node;
245
+ });
246
+
247
+ if (!changed) return { node, changed: false };
248
+ return {
249
+ node: {
250
+ ...node,
251
+ children,
252
+ },
253
+ changed: true,
254
+ };
255
+ }
256
+
257
+ export function applyCanvasEvent(
258
+ document: CanvasDocument,
259
+ event: CanvasEvent
260
+ ): CanvasDocument {
261
+ const normalizedEvent = normalizeCanvasEvent(event);
262
+ if (!normalizedEvent) return document;
263
+ event = normalizedEvent;
264
+
265
+ if (event.type === "selectNode") {
266
+ return {
267
+ ...document,
268
+ selectedNodeId: event.id,
269
+ };
270
+ }
271
+
272
+ if (event.type === "appendNode") {
273
+ if (findCanvasNodePath(document.root, event.node.id)) {
274
+ const updated = updateNodeTree(document.root, event.node.id, (node) => ({
275
+ ...node,
276
+ type: event.node.type,
277
+ props: { ...(node.props ?? {}), ...(event.node.props ?? {}) },
278
+ style: { ...(node.style ?? {}), ...(event.node.style ?? {}) },
279
+ children: event.node.children ?? node.children,
280
+ }));
281
+
282
+ return updated.changed ? { ...document, root: updated.node } : document;
283
+ }
284
+
285
+ const result = updateNodeTree(document.root, event.parentId, (node) => ({
286
+ ...node,
287
+ children: [...(node.children ?? []), event.node],
288
+ }));
289
+
290
+ if (!result.changed) return document;
291
+ return {
292
+ ...document,
293
+ root: result.node,
294
+ };
295
+ }
296
+
297
+ const result = updateNodeTree(document.root, event.id, (node) => ({
298
+ ...node,
299
+ props: event.patch.props ? { ...(node.props ?? {}), ...event.patch.props } : node.props,
300
+ style: event.patch.style ? { ...(node.style ?? {}), ...event.patch.style } : node.style,
301
+ }));
302
+
303
+ if (!result.changed) return document;
304
+ return {
305
+ ...document,
306
+ root: result.node,
307
+ };
308
+ }
@@ -0,0 +1,46 @@
1
+ export type CanvasNodeType =
2
+ | "Canvas"
3
+ | "Stack"
4
+ | "Grid"
5
+ | "Section"
6
+ | "Text"
7
+ | "Button"
8
+ | "Card"
9
+ | "MetricCard"
10
+ | "Chart"
11
+ | "List"
12
+ | "Table"
13
+ | "Toolbar";
14
+
15
+ export interface CanvasNode {
16
+ id: string;
17
+ type: CanvasNodeType;
18
+ props?: Record<string, unknown>;
19
+ style?: Record<string, string | number>;
20
+ children?: CanvasNode[];
21
+ }
22
+
23
+ export interface CanvasDocument {
24
+ version: 1;
25
+ root: CanvasNode;
26
+ selectedNodeId?: string | null;
27
+ }
28
+
29
+ export type CanvasEvent =
30
+ | {
31
+ type: "appendNode";
32
+ parentId: string;
33
+ node: CanvasNode;
34
+ }
35
+ | {
36
+ type: "updateNode";
37
+ id: string;
38
+ patch: {
39
+ props?: Record<string, unknown>;
40
+ style?: Record<string, string | number>;
41
+ };
42
+ }
43
+ | {
44
+ type: "selectNode";
45
+ id: string | null;
46
+ };
@@ -0,0 +1,52 @@
1
+ type ResolveDeleteSpaceIdArgs = {
2
+ contentKeyType: string;
3
+ docSpaceId?: string | null;
4
+ entitySpaceId?: string | null;
5
+ routeSpaceId?: string | null;
6
+ currentSpaceId?: string | null;
7
+ };
8
+
9
+ const ROUTE_SCOPED_DELETE_TYPES = new Set([
10
+ "page",
11
+ "dialog",
12
+ "file",
13
+ "image",
14
+ "meta",
15
+ ]);
16
+
17
+ export const resolveDeleteSpaceId = ({
18
+ contentKeyType,
19
+ docSpaceId,
20
+ entitySpaceId,
21
+ routeSpaceId,
22
+ currentSpaceId,
23
+ }: ResolveDeleteSpaceIdArgs): string | undefined => {
24
+ const persistedSpaceId = entitySpaceId || docSpaceId || undefined;
25
+ if (persistedSpaceId) return persistedSpaceId;
26
+ if (
27
+ routeSpaceId &&
28
+ ROUTE_SCOPED_DELETE_TYPES.has(contentKeyType)
29
+ ) {
30
+ return routeSpaceId;
31
+ }
32
+ return undefined;
33
+ };
34
+
35
+ export const resolveDeleteSuccessPath = ({
36
+ contentKey,
37
+ routeSpaceId,
38
+ }: {
39
+ contentKey?: string;
40
+ routeSpaceId?: string | null;
41
+ }): string | undefined => {
42
+ if (!contentKey || routeSpaceId) return undefined;
43
+
44
+ if (
45
+ contentKey.startsWith("cybot-pub-") ||
46
+ contentKey.startsWith("agent-pub-")
47
+ ) {
48
+ return "/explore";
49
+ }
50
+
51
+ return undefined;
52
+ };
@@ -0,0 +1,17 @@
1
+ export const shouldRenderChatSidebar = ({
2
+ isLoggedIn,
3
+ hasMounted,
4
+ useAllViewSidebar,
5
+ isLifeRoute,
6
+ }: {
7
+ isLoggedIn: boolean;
8
+ hasMounted: boolean;
9
+ useAllViewSidebar: boolean;
10
+ isLifeRoute: boolean;
11
+ }): boolean => {
12
+ if (!hasMounted) return false;
13
+ if (!isLoggedIn) return false;
14
+ if (isLifeRoute) return false;
15
+ if (useAllViewSidebar) return true;
16
+ return true;
17
+ };
@@ -0,0 +1,56 @@
1
+ import { MY_ROUTE_SECTIONS } from "app/constants/mySections";
2
+
3
+ const allViewRoutePaths = new Set([
4
+ "/",
5
+ ...MY_ROUTE_SECTIONS.map((section) => section.path),
6
+ "/share/mine",
7
+ ]);
8
+
9
+ const SPACE_ROUTE_PATTERN = /^\/space\/([^/]+)(?:\/(.*))?\/?$/;
10
+
11
+ export interface SpaceRouteContext {
12
+ isSpaceRoute: boolean;
13
+ isSpaceRootRoute: boolean;
14
+ spaceId: string | null;
15
+ }
16
+
17
+ export function isAllViewRoutePath(pathname: string): boolean {
18
+ return allViewRoutePaths.has(pathname);
19
+ }
20
+
21
+ export function getSpaceRouteContext(pathname: string): SpaceRouteContext {
22
+ const match = pathname.match(SPACE_ROUTE_PATTERN);
23
+ if (!match) {
24
+ return {
25
+ isSpaceRoute: false,
26
+ isSpaceRootRoute: false,
27
+ spaceId: null,
28
+ };
29
+ }
30
+
31
+ const [, encodedSpaceId, descendantPath] = match;
32
+
33
+ let spaceId = encodedSpaceId;
34
+ try {
35
+ spaceId = decodeURIComponent(encodedSpaceId);
36
+ } catch {
37
+ // Keep the encoded segment so route classification never fails on bad input.
38
+ }
39
+
40
+ return {
41
+ isSpaceRoute: true,
42
+ isSpaceRootRoute: !descendantPath,
43
+ spaceId,
44
+ };
45
+ }
46
+
47
+ export function isSpaceRoutePath(pathname: string): boolean {
48
+ return getSpaceRouteContext(pathname).isSpaceRoute;
49
+ }
50
+
51
+ export function shouldForceCategoriesViewMode(
52
+ pathname: string,
53
+ viewMode: string
54
+ ): boolean {
55
+ return isSpaceRoutePath(pathname) && viewMode !== "categories";
56
+ }
@@ -0,0 +1,87 @@
1
+ import { useMemo } from "react";
2
+
3
+ export type ContentKeyType =
4
+ | "app"
5
+ | "page"
6
+ | "meta"
7
+ | "dialog"
8
+ | "image"
9
+ | "file"
10
+ | "agent"
11
+ | "cybot"
12
+ | "other"
13
+ | "unknown";
14
+
15
+ export interface TopBarProps {
16
+ toggleSidebar?: () => void;
17
+ isSidebarOpen?: boolean;
18
+ }
19
+
20
+ export const getSideChatLabels = (
21
+ t: (key: string, fallback?: string) => string,
22
+ type: ContentKeyType
23
+ ) => {
24
+ switch (type) {
25
+ case "app":
26
+ return {
27
+ open: t("showAppAssistant", "打开应用 AI"),
28
+ hide: t("hideAppAssistant", "隐藏应用 AI"),
29
+ };
30
+ case "page":
31
+ return {
32
+ open: t("showDocAssistant", "打开文档 AI"),
33
+ hide: t("hideDocAssistant", "隐藏文档 AI"),
34
+ };
35
+ case "meta":
36
+ return {
37
+ open: t("showTableAssistant", "打开表格 AI"),
38
+ hide: t("hideTableAssistant", "隐藏表格 AI"),
39
+ };
40
+ case "image":
41
+ return {
42
+ open: t("showImageAssistant", "打开图片 AI"),
43
+ hide: t("hideImageAssistant", "隐藏图片 AI"),
44
+ };
45
+ case "file":
46
+ return {
47
+ open: t("showFileAssistant", "打开文件 AI"),
48
+ hide: t("hideFileAssistant", "隐藏文件 AI"),
49
+ };
50
+ default:
51
+ return {
52
+ open: t("showAssistant", "打开页面 AI"),
53
+ hide: t("hideAssistant", "隐藏页面 AI"),
54
+ };
55
+ }
56
+ };
57
+
58
+ /** 判断是否 Mac,用于快捷键提示 */
59
+ export const useIsMac = (): boolean =>
60
+ useMemo(
61
+ () =>
62
+ typeof window !== "undefined" &&
63
+ /Mac|iPod|iPhone|iPad/.test(window.navigator.platform),
64
+ []
65
+ );
66
+
67
+ /** 根据路由中的 pageKey 推断当前内容类型 */
68
+ export const useContentKeyType = (
69
+ pageKey?: string,
70
+ pageType?: string,
71
+ appKey?: string
72
+ ): ContentKeyType =>
73
+ useMemo(() => {
74
+ if (appKey) return "app";
75
+ if (!pageKey) return "unknown";
76
+ if (pageType === "image") return "image";
77
+ if (pageType === "file") return "file";
78
+ if (pageKey.startsWith("file")) return "file";
79
+ if (pageKey.startsWith("image")) return "image";
80
+ if (pageKey.startsWith("page")) return "page";
81
+ if (pageKey.startsWith("meta")) return "meta";
82
+ if (pageKey.startsWith("dialog")) return "dialog";
83
+ if (pageKey.startsWith("task")) return "task";
84
+ if (pageKey.startsWith("agent")) return "agent";
85
+ if (pageKey.startsWith("cybot")) return "cybot";
86
+ return "other";
87
+ }, [appKey, pageKey, pageType]);