@olares/lares-mobile 0.15.5 → 0.15.7

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 (44) hide show
  1. package/README.md +6 -2
  2. package/package.json +2 -2
  3. package/src/App.vue +918 -178
  4. package/src/chat/ChatBar.vue +2 -2
  5. package/src/chat/Composer.vue +467 -16
  6. package/src/chat/Deliverables.vue +22 -10
  7. package/src/chat/HistoryPanel.vue +747 -55
  8. package/src/chat/MessageActions.vue +88 -34
  9. package/src/chat/QuestionCard.vue +5 -5
  10. package/src/chat/StageRow.vue +8 -5
  11. package/src/chat/Transcript.vue +20 -6
  12. package/src/chat/composer-tokens.js +32 -0
  13. package/src/chat/format.js +37 -0
  14. package/src/desktop/DesktopComposer.vue +942 -0
  15. package/src/desktop/DesktopContextMeter.vue +81 -0
  16. package/src/desktop/DesktopGoal.vue +88 -0
  17. package/src/desktop/DesktopPreview.vue +58 -0
  18. package/src/desktop/DesktopPreviewTabs.vue +82 -0
  19. package/src/desktop/DesktopQuestions.vue +118 -0
  20. package/src/desktop/DesktopQueue.vue +203 -0
  21. package/src/desktop/DesktopShell.vue +229 -0
  22. package/src/desktop/DesktopSidebar.vue +645 -0
  23. package/src/desktop/DesktopTodos.vue +52 -0
  24. package/src/desktop/DirectoryBrowser.vue +231 -0
  25. package/src/desktop/DirectoryNewFolder.vue +124 -0
  26. package/src/desktop/ui/PcDialog.vue +100 -0
  27. package/src/desktop/ui/PcPopover.vue +110 -0
  28. package/src/desktop/ui/PcTooltip.vue +95 -0
  29. package/src/desktop/ui/place.js +24 -0
  30. package/src/desktop/workspace-view.js +121 -0
  31. package/src/host.js +20 -2
  32. package/src/index.d.ts +2 -0
  33. package/src/mobile/MobileDirectoryNewFolder.vue +121 -0
  34. package/src/mobile/MobileDirectorySheet.vue +162 -0
  35. package/src/mobile/MobileNewSession.vue +217 -0
  36. package/src/mobile/MobileShell.vue +367 -0
  37. package/src/motion.css +166 -0
  38. package/src/preview/Preview.vue +91 -150
  39. package/src/preview/PreviewBody.vue +185 -0
  40. package/src/settings/AgentSettings.vue +126 -36
  41. package/src/settings/Select.vue +183 -0
  42. package/src/settings/SettingRow.vue +5 -4
  43. package/src/settings/Sheet.vue +4 -4
  44. package/src/styles.css +9 -0
@@ -1,56 +1,75 @@
1
1
  <template>
2
2
  <div class="lares-actions" role="toolbar" :aria-label="t('action.bar')">
3
3
  <div class="lares-actions__left">
4
- <button type="button" :aria-label="copied ? t('action.copied') : t('action.copy')" @click="copy">
5
- <svg viewBox="0 0 24 24" aria-hidden="true">
6
- <rect x="9" y="9" width="11" height="13" rx="2" />
7
- <path d="M6 16H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
8
- </svg>
9
- </button>
10
- <button
11
- type="button"
12
- :aria-label="t('action.like')"
13
- :aria-pressed="reaction === 'like' ? 'true' : 'false'"
14
- :data-on="reaction === 'like' ? 'true' : 'false'"
15
- @click="$emit('react', reaction === 'like' ? '' : 'like')"
16
- >
17
- <svg viewBox="0 0 24 24" aria-hidden="true">
18
- <path d="M7 11v10H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h3Zm0 0 4.2-8.1A2.2 2.2 0 0 1 15.2 4v5H20a2 2 0 0 1 2 2.3l-1.2 8A2 2 0 0 1 18.8 21H7" />
19
- </svg>
20
- </button>
21
- <button
22
- type="button"
23
- :aria-label="t('action.dislike')"
24
- :aria-pressed="reaction === 'dislike' ? 'true' : 'false'"
25
- :data-on="reaction === 'dislike' ? 'true' : 'false'"
26
- @click="$emit('react', reaction === 'dislike' ? '' : 'dislike')"
27
- >
28
- <svg viewBox="0 0 24 24" aria-hidden="true">
29
- <path d="M17 13V3h3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-3Zm0 0-4.2 8.1A2.2 2.2 0 0 1 8.8 20v-5H4a2 2 0 0 1-2-2.3l1.2-8A2 2 0 0 1 5.2 3H17" />
30
- </svg>
31
- </button>
32
- <button type="button" :aria-label="t('action.share')" @click="share">
33
- <svg viewBox="0 0 24 24" aria-hidden="true">
34
- <path d="M4 20v-7a4 4 0 0 1 4-4h12" />
35
- <path d="m16 5 4 4-4 4" />
36
- </svg>
37
- </button>
4
+ <LaresPcTooltip :label="copied ? t('action.copied') : t('action.copy')">
5
+ <button type="button" :aria-label="copied ? t('action.copied') : t('action.copy')" @click="copy">
6
+ <svg viewBox="0 0 24 24" aria-hidden="true">
7
+ <rect x="9" y="9" width="11" height="13" rx="2" />
8
+ <path d="M6 16H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
9
+ </svg>
10
+ </button>
11
+ </LaresPcTooltip>
12
+ <LaresPcTooltip :label="t('action.like')">
13
+ <button
14
+ type="button"
15
+ :aria-label="t('action.like')"
16
+ :aria-pressed="reaction === 'like' ? 'true' : 'false'"
17
+ :data-on="reaction === 'like' ? 'true' : 'false'"
18
+ @click="$emit('react', reaction === 'like' ? '' : 'like')"
19
+ >
20
+ <svg viewBox="0 0 24 24" aria-hidden="true">
21
+ <path d="M7 11v10H4a1 1 0 0 1-1-1v-8a1 1 0 0 1 1-1h3Zm0 0 4.2-8.1A2.2 2.2 0 0 1 15.2 4v5H20a2 2 0 0 1 2 2.3l-1.2 8A2 2 0 0 1 18.8 21H7" />
22
+ </svg>
23
+ </button>
24
+ </LaresPcTooltip>
25
+ <LaresPcTooltip :label="t('action.dislike')">
26
+ <button
27
+ type="button"
28
+ :aria-label="t('action.dislike')"
29
+ :aria-pressed="reaction === 'dislike' ? 'true' : 'false'"
30
+ :data-on="reaction === 'dislike' ? 'true' : 'false'"
31
+ @click="$emit('react', reaction === 'dislike' ? '' : 'dislike')"
32
+ >
33
+ <svg viewBox="0 0 24 24" aria-hidden="true">
34
+ <path d="M17 13V3h3a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-3Zm0 0-4.2 8.1A2.2 2.2 0 0 1 8.8 20v-5H4a2 2 0 0 1-2-2.3l1.2-8A2 2 0 0 1 5.2 3H17" />
35
+ </svg>
36
+ </button>
37
+ </LaresPcTooltip>
38
+ <LaresPcTooltip :label="t('action.share')">
39
+ <button type="button" :aria-label="t('action.share')" @click="share">
40
+ <svg viewBox="0 0 24 24" aria-hidden="true">
41
+ <path d="M4 20v-7a4 4 0 0 1 4-4h12" />
42
+ <path d="m16 5 4 4-4 4" />
43
+ </svg>
44
+ </button>
45
+ </LaresPcTooltip>
38
46
  </div>
47
+ <span v-if="stats" class="lares-actions__stats">{{ stats }}</span>
39
48
  </div>
40
49
  </template>
41
50
 
42
51
  <script>
52
+ import { metricsLine } from "./format.js";
53
+ import LaresPcTooltip from "../desktop/ui/PcTooltip.vue";
54
+
43
55
  export default {
44
56
  name: "LaresMessageActions",
57
+ components: { LaresPcTooltip },
45
58
  props: {
46
59
  text: { type: String, default: "" },
47
60
  reaction: { type: String, default: "" },
61
+ metrics: { type: Object, default: null },
48
62
  t: { type: Function, required: true },
49
63
  },
50
64
  emits: ["react"],
51
65
  data() {
52
66
  return { copied: false, copyTimer: 0 };
53
67
  },
68
+ computed: {
69
+ stats() {
70
+ return metricsLine(this.metrics, this.t);
71
+ },
72
+ },
54
73
  beforeUnmount() {
55
74
  if (this.copyTimer) clearTimeout(this.copyTimer);
56
75
  },
@@ -117,6 +136,41 @@ export default {
117
136
  background: var(--q-btn-bg-pressed);
118
137
  }
119
138
 
139
+ /* Pointer chrome only: on touch there is no hover to reveal the readings, and
140
+ the mobile reply keeps its bare row of icons. */
141
+ .lares-actions__stats {
142
+ display: none;
143
+ }
144
+
145
+ @media (hover: hover) {
146
+ .lares-actions__stats {
147
+ display: inline;
148
+ opacity: 0;
149
+ transition: opacity 80ms ease;
150
+ color: var(--q-ink-3);
151
+ font-size: 13px;
152
+ white-space: nowrap;
153
+ }
154
+
155
+ .lares-actions button {
156
+ cursor: pointer;
157
+ }
158
+
159
+ .lares-actions button:hover {
160
+ background: var(--q-background-hover);
161
+ color: var(--q-ink-2);
162
+ }
163
+
164
+ .lares-actions button[data-on="true"]:hover {
165
+ color: var(--q-blue-default);
166
+ }
167
+
168
+ .lares-actions:hover .lares-actions__stats,
169
+ .lares-actions:focus-within .lares-actions__stats {
170
+ opacity: 1;
171
+ }
172
+ }
173
+
120
174
  .lares-actions svg {
121
175
  width: 18px;
122
176
  height: 18px;
@@ -74,13 +74,13 @@ export default {
74
74
  .lares-question__heading {
75
75
  margin: 0;
76
76
  color: var(--q-ink-2);
77
- font-size: 12px;
77
+ font-size: 14px;
78
78
  }
79
79
 
80
80
  .lares-question__prompt {
81
81
  margin: 0;
82
82
  color: var(--q-ink-1);
83
- font-size: 15px;
83
+ font-size: 17px;
84
84
  font-weight: 500;
85
85
  }
86
86
 
@@ -88,7 +88,7 @@ export default {
88
88
  .lares-question__wait {
89
89
  margin: 0;
90
90
  color: var(--q-ink-3);
91
- font-size: 13px;
91
+ font-size: 15px;
92
92
  }
93
93
 
94
94
  .lares-question__options {
@@ -115,12 +115,12 @@ export default {
115
115
  }
116
116
 
117
117
  .lares-question__label {
118
- font-size: 14px;
118
+ font-size: 16px;
119
119
  color: var(--q-ink-1);
120
120
  }
121
121
 
122
122
  .lares-question__hint {
123
- font-size: 12px;
123
+ font-size: 14px;
124
124
  color: var(--q-ink-3);
125
125
  }
126
126
  </style>
@@ -78,8 +78,8 @@ export default {
78
78
  .lares-stage__summary {
79
79
  min-width: 0;
80
80
  overflow: hidden;
81
- font-size: 14px;
82
- line-height: 24px;
81
+ font-size: 16px;
82
+ line-height: 26px;
83
83
  text-overflow: ellipsis;
84
84
  white-space: nowrap;
85
85
  }
@@ -106,14 +106,14 @@ export default {
106
106
  box-sizing: border-box;
107
107
  min-width: 0;
108
108
  max-width: 100%;
109
- max-height: 141px;
109
+ max-height: 156px;
110
110
  margin: 4px 0 0 22px;
111
111
  border-radius: 8px;
112
112
  padding: 10px 12px;
113
113
  overflow: auto;
114
114
  background: var(--q-background-3);
115
115
  color: var(--q-ink-2);
116
- font: 400 11px/16px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
116
+ font: 400 13px/18px ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
117
117
  white-space: pre-wrap;
118
118
  overflow-wrap: anywhere;
119
119
  word-break: break-word;
@@ -126,6 +126,9 @@ export default {
126
126
  border-left: 2px solid var(--q-input-stroke);
127
127
  padding: 0 0 0 12px;
128
128
  background: transparent;
129
- font: 400 13px/1.6 inherit;
129
+ font-family: inherit;
130
+ font-size: 15px;
131
+ font-weight: 400;
132
+ line-height: 1.6;
130
133
  }
131
134
  </style>
@@ -33,7 +33,7 @@
33
33
  :body="row.text"
34
34
  />
35
35
  <LaresQuestionCard
36
- v-else-if="row.type === 'tool' && row.name === 'ask_user_question' && row.status === 'running'"
36
+ v-else-if="!questionTakeover && row.type === 'tool' && row.name === 'ask_user_question' && row.status === 'running'"
37
37
  :questions="questionItems(row)"
38
38
  :rpc-id="question?.rpcId || ''"
39
39
  :busy="questionBusy"
@@ -64,6 +64,7 @@
64
64
  <LaresMessageActions
65
65
  :text="row.text"
66
66
  :reaction="reactions[actionKey(index)] || ''"
67
+ :metrics="row.metrics || null"
67
68
  :t="t"
68
69
  @react="setReaction(index, $event)"
69
70
  />
@@ -106,6 +107,7 @@ export default {
106
107
  sessionId: { type: String, default: "" },
107
108
  question: { type: Object, default: null },
108
109
  questionBusy: { type: Boolean, default: false },
110
+ questionTakeover: { type: Boolean, default: false },
109
111
  t: { type: Function, required: true },
110
112
  },
111
113
  emits: ["open", "media", "answer"],
@@ -225,7 +227,7 @@ export default {
225
227
 
226
228
  .lares-log__empty {
227
229
  margin: 24px 0 0;
228
- font-size: 13px;
230
+ font-size: 15px;
229
231
  color: var(--q-ink-3);
230
232
  }
231
233
 
@@ -267,7 +269,8 @@ export default {
267
269
  }
268
270
 
269
271
  @media (prefers-reduced-motion: reduce) {
270
- .lares-sk {
272
+ .lares-sk,
273
+ .lares-log__item[data-pending="true"] {
271
274
  animation: none;
272
275
  }
273
276
  }
@@ -280,6 +283,17 @@ export default {
280
283
  max-width: 100%;
281
284
  }
282
285
 
286
+ .lares-log__item[data-pending="true"] {
287
+ animation: lares-msg-in var(--lares-duration-normal, 220ms) var(--lares-ease-out, cubic-bezier(0.32, 0.72, 0, 1));
288
+ }
289
+
290
+ @keyframes lares-msg-in {
291
+ from {
292
+ opacity: 0;
293
+ transform: translateY(8px);
294
+ }
295
+ }
296
+
283
297
  .lares-log__item[data-type="user"],
284
298
  .lares-log__item[data-type="assistant"],
285
299
  .lares-log__item[data-type="files"] {
@@ -305,7 +319,7 @@ export default {
305
319
  min-width: 0;
306
320
  max-width: 100%;
307
321
  overflow-x: hidden;
308
- font-size: 15px;
322
+ font-size: 17px;
309
323
  line-height: 1.55;
310
324
  word-break: break-word;
311
325
  overflow-wrap: anywhere;
@@ -350,7 +364,7 @@ export default {
350
364
  .lares-md :deep(h2),
351
365
  .lares-md :deep(h3) {
352
366
  margin: 0 0 8px;
353
- font-size: 16px;
367
+ font-size: 18px;
354
368
  font-weight: 600;
355
369
  }
356
370
 
@@ -358,7 +372,7 @@ export default {
358
372
  border-radius: 4px;
359
373
  padding: 1px 4px;
360
374
  background: var(--q-background-3);
361
- font-size: 13px;
375
+ font-size: 15px;
362
376
  }
363
377
 
364
378
  .lares-md :deep(pre) {
@@ -0,0 +1,32 @@
1
+ import { activeReferenceToken, insertReference } from "@olares/lares-core/larepass/references";
2
+
3
+ export const REFERENCE_DEBOUNCE_MS = 100;
4
+
5
+ export function referenceRows(references, t) {
6
+ let previous = "";
7
+ return (references ?? []).map((row) => {
8
+ const group = row.kind === "session" ? "sessions" : "files";
9
+ const section = group === previous ? "" : t(`references.section.${group}`);
10
+ previous = group;
11
+ return { ...row, section, name: `${t(`references.${row.kind}`)} · ${row.label}` };
12
+ });
13
+ }
14
+
15
+ export function visibleCommands(commands, draft, commandTyped) {
16
+ const rows = commands ?? [];
17
+ if (!commandTyped) return rows;
18
+ const needle = String(draft).match(/^\/([^\s]*)$/)?.[1]?.toLowerCase() ?? "";
19
+ return rows.filter((row) => row.name.toLowerCase().includes(needle));
20
+ }
21
+
22
+ export function isCommandLine(draft) {
23
+ return /^\/[^\s]*$/.test(String(draft ?? ""));
24
+ }
25
+
26
+ export function activeReference(draft, caret) {
27
+ return activeReferenceToken(draft, caret);
28
+ }
29
+
30
+ export function applyReference(draft, token, row) {
31
+ return insertReference(draft, token, row);
32
+ }
@@ -1,5 +1,11 @@
1
1
  import { interpolate } from "@olares/lares-core/i18n/t";
2
2
  import { STAGE_COPY } from "@olares/lares-core/larepass/stage-copy";
3
+ import {
4
+ clockParts,
5
+ durationParts,
6
+ latencySeconds,
7
+ throughputTokens,
8
+ } from "@olares/lares-core/larepass/turn-metrics";
3
9
 
4
10
  export function withPendingUser(items, pendingUser) {
5
11
  if (!pendingUser) return items;
@@ -50,6 +56,37 @@ export function failText(t, failed, error) {
50
56
  return t("chat.failed", { error: failed || error });
51
57
  }
52
58
 
59
+ function clockText(time, t, now) {
60
+ const { clock, scope, y, m, d } = clockParts(time, now);
61
+ if (scope === "day") return clock;
62
+ return `${t(scope === "md" ? "clock.md" : "clock.ymd", { y, m, d })} ${clock}`;
63
+ }
64
+
65
+ /**
66
+ * The reply's own footer: when it landed, and whatever the log recorded about
67
+ * how it got there. Each reading is dropped rather than faked when the events
68
+ * never carried it.
69
+ */
70
+ export function metricsLine(metrics, t, now = Date.now()) {
71
+ if (!metrics?.time) return "";
72
+ const parts = [clockText(metrics.time, t, now)];
73
+ if (metrics.runMs !== undefined) {
74
+ const { minutes, seconds } = durationParts(metrics.runMs);
75
+ parts.push(t("message.ranFor", {
76
+ duration: minutes > 0
77
+ ? t("duration.minutes", { minutes, seconds })
78
+ : t("duration.seconds", { seconds }),
79
+ }));
80
+ }
81
+ if (metrics.ttftMs !== undefined) {
82
+ parts.push(t("message.ttft", { seconds: latencySeconds(metrics.ttftMs) }));
83
+ }
84
+ if (metrics.tokensPerSecond !== undefined) {
85
+ parts.push(t("message.tokensPerSecond", { tps: throughputTokens(metrics.tokensPerSecond) }));
86
+ }
87
+ return parts.join(" · ");
88
+ }
89
+
53
90
  export function retryStatus(row) {
54
91
  const copy = STAGE_COPY.retry;
55
92
  const label = row.retryState === "started"