@agentprojectcontext/apx 1.78.0 → 1.79.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/run-agent.js +30 -5
  3. package/src/core/agent/tool-summary.js +65 -0
  4. package/src/core/agent/tools/handlers/list-commitments.js +80 -0
  5. package/src/core/agent/tools/handlers/list-tasks.js +66 -27
  6. package/src/core/agent/tools/handlers/record-commitment.js +68 -0
  7. package/src/core/agent/tools/handlers/send-telegram.js +68 -2
  8. package/src/core/agent/tools/names.js +6 -0
  9. package/src/core/agent/tools/registry.js +9 -0
  10. package/src/core/agent/tools/tool-call-parser.js +70 -1
  11. package/src/core/channels/telegram/ask-callbacks.js +35 -0
  12. package/src/core/channels/telegram/dispatch.js +3 -0
  13. package/src/core/channels/telegram/reply.js +30 -5
  14. package/src/core/config/paths.js +3 -0
  15. package/src/core/config/redact.js +22 -0
  16. package/src/core/daemon/service.js +238 -0
  17. package/src/core/engines/gemini.js +322 -60
  18. package/src/core/engines/openai-compatible.js +21 -2
  19. package/src/core/memory/consolidate.js +225 -0
  20. package/src/core/nudge/index.js +192 -0
  21. package/src/core/nudge/policy.js +143 -0
  22. package/src/core/nudge/store.js +141 -0
  23. package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
  24. package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
  25. package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
  26. package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
  27. package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
  28. package/src/core/routines/runner.js +102 -3
  29. package/src/core/routines/signals.js +270 -0
  30. package/src/core/stores/commitments.js +331 -0
  31. package/src/core/stores/messages.js +4 -0
  32. package/src/core/stores/routines.js +17 -3
  33. package/src/core/util/thinking.js +51 -0
  34. package/src/host/daemon/api/commitments.js +135 -0
  35. package/src/host/daemon/api/nudges.js +112 -0
  36. package/src/host/daemon/api/routines.js +24 -0
  37. package/src/host/daemon/api/self-memory.js +50 -0
  38. package/src/host/daemon/api/telegram.js +42 -4
  39. package/src/host/daemon/api/voice.js +3 -1
  40. package/src/host/daemon/api.js +6 -0
  41. package/src/host/daemon/callback-reconciler.js +16 -0
  42. package/src/host/daemon/plugins/desktop/index.js +7 -1
  43. package/src/host/daemon/plugins/telegram/index.js +7 -2
  44. package/src/host/daemon/wakeup.js +17 -3
  45. package/src/interfaces/cli/commands/commitment.js +154 -0
  46. package/src/interfaces/cli/commands/daemon.js +57 -0
  47. package/src/interfaces/cli/commands/memory.js +73 -0
  48. package/src/interfaces/cli/commands/nudge.js +130 -0
  49. package/src/interfaces/cli/help/index.js +2 -2
  50. package/src/interfaces/cli/routes/commitment.js +19 -0
  51. package/src/interfaces/cli/routes/daemon.js +7 -1
  52. package/src/interfaces/cli/routes/index.js +4 -0
  53. package/src/interfaces/cli/routes/memory.js +10 -2
  54. package/src/interfaces/cli/routes/nudge.js +17 -0
  55. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
  56. package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
  57. package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
  58. package/src/interfaces/web/dist/index.html +2 -2
  59. package/src/interfaces/web/package-lock.json +11 -10
  60. package/src/interfaces/web/src/components/Section.tsx +18 -3
  61. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
  62. package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
  63. package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
  64. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +34 -4
  65. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
  66. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +12 -2
  67. package/src/interfaces/web/src/components/routines/shared.ts +14 -5
  68. package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
  69. package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
  70. package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
  71. package/src/interfaces/web/src/components/ui.tsx +1 -0
  72. package/src/interfaces/web/src/constants/index.ts +1 -0
  73. package/src/interfaces/web/src/hooks/useChat.ts +5 -1
  74. package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
  75. package/src/interfaces/web/src/i18n/en.ts +127 -0
  76. package/src/interfaces/web/src/i18n/es.ts +127 -0
  77. package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
  78. package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
  79. package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
  80. package/src/interfaces/web/src/lib/cron.ts +196 -0
  81. package/src/interfaces/web/src/lib/when.ts +32 -0
  82. package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
  83. package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
  84. package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
  85. package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
  86. package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +102 -19
  87. package/src/interfaces/web/src/screens/base/LogsTab.tsx +15 -0
  88. package/src/interfaces/web/src/screens/project/ChatTab.tsx +21 -3
  89. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +13 -11
  90. package/src/interfaces/web/src/types/daemon.ts +10 -1
  91. package/src/interfaces/web/dist/assets/index-CBR_-QyA.js +0 -824
  92. package/src/interfaces/web/dist/assets/index-CBR_-QyA.js.map +0 -1
  93. package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
@@ -1,10 +1,14 @@
1
1
  import { useState } from "react";
2
+ import { Plus } from "lucide-react";
2
3
  import { useNavigate } from "react-router-dom";
3
4
  import { Tasks } from "../../lib/api";
4
5
  import type { TaskStatus } from "../../types/daemon";
5
6
  import { Section } from "../../components/Section";
6
7
  import { PagedList, usePagedQuery } from "../../components/Pager";
7
- import { Badge, Button, Empty, Loading } from "../../components/ui";
8
+ import { Badge, Button, Dialog, Empty, Field, FilterChips, Input, Loading } from "../../components/ui";
9
+ import { UiSelect } from "../../components/UiSelect";
10
+ import { useProjects } from "../../hooks/useProjects";
11
+ import { useToast } from "../../components/Toast";
8
12
  import { t } from "../../i18n";
9
13
 
10
14
  // All projects' tasks, aggregated (GET /tasks), server-paginated.
@@ -15,38 +19,79 @@ export function GlobalTasksTab() {
15
19
  // right now" is not "what is open". Only meaningful for open tasks.
16
20
  const [status, setStatus] = useState<TaskStatus | "">("");
17
21
  const effectiveStatus = state === "open" ? status : "";
22
+ // The per-project Tasks screen has always had an inline add form; this one
23
+ // did not, so on the base workspace the only way to file a task was to ask
24
+ // the agent. A picker is the price of the cross-project view.
25
+ const [adding, setAdding] = useState(false);
26
+ const [title, setTitle] = useState("");
27
+ const [target, setTarget] = useState("");
28
+ const [due, setDue] = useState("");
29
+ const [busy, setBusy] = useState(false);
30
+ const { projects } = useProjects();
31
+ const toast = useToast();
32
+
18
33
  const paged = usePagedQuery({
19
34
  key: `/api/tasks?state=${state}&status=${effectiveStatus}`,
20
35
  fetchPage: (limit, offset) => Tasks.globalPage({ state, limit, offset, status: effectiveStatus }),
21
36
  resetKey: `${state}|${effectiveStatus}`,
22
37
  });
23
38
 
39
+ const create = async () => {
40
+ if (!title.trim() || !target) return;
41
+ setBusy(true);
42
+ try {
43
+ await Tasks.add(String(target), { title: title.trim(), due: due || null });
44
+ await paged.mutate();
45
+ setAdding(false);
46
+ setTitle(""); setDue("");
47
+ } catch (e) {
48
+ toast.error((e as Error).message);
49
+ } finally {
50
+ setBusy(false);
51
+ }
52
+ };
53
+
24
54
  return (
25
55
  <Section
26
56
  fullHeight
27
57
  title={t("project.global_tasks.title")}
28
58
  description={t("project.global_tasks.subtitle")}
29
59
  action={
30
- <div className="flex gap-1">
31
- {(["open", "done", "dropped", "all"] as const).map((s) => (
32
- <Button key={s} size="sm" variant={state === s ? "primary" : "ghost"} onClick={() => setState(s)}>{s}</Button>
33
- ))}
34
- </div>
60
+ <Button
61
+ size="sm"
62
+ variant="primary"
63
+ onClick={() => { setTarget(String(projects[0]?.id ?? "")); setAdding(true); }}
64
+ >
65
+ <Plus size={14} /> {t("project.global_tasks.add")}
66
+ </Button>
67
+ }
68
+ filters={
69
+ <>
70
+ <FilterChips
71
+ value={state}
72
+ onChange={setState}
73
+ label={t("project.global_tasks.title")}
74
+ options={(["open", "done", "dropped", "all"] as const).map((s) => ({ value: s, label: s }))}
75
+ />
76
+ {state === "open" ? (
77
+ <>
78
+ <span className="mx-1 h-4 w-px bg-border" aria-hidden />
79
+ <FilterChips
80
+ value={status}
81
+ onChange={setStatus}
82
+ label={t("project.global_tasks.any_status")}
83
+ options={[
84
+ { value: "" as const, label: t("project.global_tasks.any_status") },
85
+ ...(["pending", "running", "in_review", "blocked"] as const).map((s) => ({
86
+ value: s, label: s.replace("_", " "),
87
+ })),
88
+ ]}
89
+ />
90
+ </>
91
+ ) : null}
92
+ </>
35
93
  }
36
94
  >
37
- {state === "open" ? (
38
- <div className="mb-3 flex flex-wrap gap-1">
39
- <Button size="sm" variant={status === "" ? "primary" : "ghost"} onClick={() => setStatus("")}>
40
- {t("project.global_tasks.any_status")}
41
- </Button>
42
- {(["pending", "running", "in_review", "blocked"] as const).map((s) => (
43
- <Button key={s} size="sm" variant={status === s ? "primary" : "ghost"} onClick={() => setStatus(s)}>
44
- {s.replace("_", " ")}
45
- </Button>
46
- ))}
47
- </div>
48
- ) : null}
49
-
50
95
  {paged.isLoading && <Loading />}
51
96
  {!paged.isLoading && paged.total === 0 && <Empty>{t("project.global_tasks.empty")}</Empty>}
52
97
  <PagedList paged={paged} fullHeight>
@@ -73,6 +118,44 @@ export function GlobalTasksTab() {
73
118
  ))}
74
119
  </ul>
75
120
  </PagedList>
121
+
122
+ <Dialog
123
+ open={adding}
124
+ onClose={() => setAdding(false)}
125
+ title={t("project.global_tasks.add_title")}
126
+ footer={
127
+ <>
128
+ <Button onClick={() => setAdding(false)}>{t("common.cancel")}</Button>
129
+ <Button variant="primary" loading={busy} disabled={!title.trim() || !target} onClick={create}>
130
+ {t("project.global_tasks.add")}
131
+ </Button>
132
+ </>
133
+ }
134
+ >
135
+ <div className="flex flex-col gap-3 p-5">
136
+ <Field label={t("project.global_tasks.field_title")}>
137
+ <Input
138
+ value={title}
139
+ onChange={(e) => setTitle(e.target.value)}
140
+ autoFocus
141
+ onKeyDown={(e) => { if (e.key === "Enter" && title.trim() && target) create(); }}
142
+ />
143
+ </Field>
144
+ {/* Required, unlike the per-project screen: a cross-project view has
145
+ no "here" to default to, and guessing one would file the task
146
+ somewhere the user did not look. */}
147
+ <Field label={t("project.global_tasks.field_project")}>
148
+ <UiSelect
149
+ value={String(target)}
150
+ onChange={setTarget}
151
+ options={projects.map((p) => ({ value: String(p.id), label: p.name || p.path }))}
152
+ />
153
+ </Field>
154
+ <Field label={t("project.global_tasks.field_due")}>
155
+ <Input type="date" value={due} onChange={(e) => setDue(e.target.value)} />
156
+ </Field>
157
+ </div>
158
+ </Dialog>
76
159
  </Section>
77
160
  );
78
161
  }
@@ -23,6 +23,16 @@ function actorLabel(m: MessageEntry): string {
23
23
  return m.agent_slug || m.actor_id || m.author || m.actor_kind || "—";
24
24
  }
25
25
 
26
+ /** Structured lifecycle events APX records as they happen (a routine created,
27
+ * a routine updated). Returns the label to badge, or null for ordinary rows. */
28
+ function lifecycleEvent(m: MessageEntry): string | null {
29
+ const e = m.meta?.event;
30
+ if (typeof e !== "string") return null;
31
+ if (e === "routine_created") return t("logs.event_routine_created");
32
+ if (e === "routine_updated") return t("logs.event_routine_updated");
33
+ return null;
34
+ }
35
+
26
36
  /** Model that produced this record — persisted in meta by every channel that
27
37
  * runs an agent turn. Absent on user/system rows and on pre-1.74 history. */
28
38
  function modelOf(m: MessageEntry): string | null {
@@ -56,6 +66,11 @@ function LogRow({ m }: { m: MessageEntry }) {
56
66
  <span className="font-mono">{fmtTs(m.ts)}</span>
57
67
  <Badge tone="info">{m.channel}</Badge>
58
68
  {m.type && <Badge>{m.type}</Badge>}
69
+ {/* Lifecycle events get their own chip rather than blending into the
70
+ body text. "A routine now exists" is the kind of thing people
71
+ scroll back looking for, and a green badge is findable in a way a
72
+ sentence in a log line is not. */}
73
+ {lifecycleEvent(m) && <Badge tone="success">{lifecycleEvent(m)}</Badge>}
59
74
  <span className="font-medium text-foreground">{actorLabel(m)}</span>
60
75
  {model && <span className="font-mono text-[11px] text-sky-400/90">{model}</span>}
61
76
  {tokens !== null && <span className="font-mono text-[11px]">{tokens} tok</span>}
@@ -20,7 +20,21 @@ import type { AgentEntry } from "../../types/daemon";
20
20
  // with a real APC agent slug (which must match /^[a-z][a-z0-9_-]*$/).
21
21
  const ROBY_SLUG = "__super_agent__";
22
22
 
23
- export function ChatTab({ pid }: { pid: string }) {
23
+ /**
24
+ * `hideSidebar` lets another screen (the agent inbox) supply its own
25
+ * conversation list and embed just the thread. The chat surface is not forked
26
+ * — one implementation, two frames around it.
27
+ */
28
+ export function ChatTab({
29
+ pid,
30
+ hideSidebar = false,
31
+ initialSelection,
32
+ }: {
33
+ pid: string;
34
+ hideSidebar?: boolean;
35
+ /** Open a specific conversation on mount instead of a fresh live session. */
36
+ initialSelection?: ChatKey;
37
+ }) {
24
38
  const toast = useToast();
25
39
  const [params, setSearchParams] = useSearchParams();
26
40
  const agents = useSWR(`/api/projects/${pid}/agents`, () => Agents.list(pid));
@@ -36,6 +50,10 @@ export function ChatTab({ pid }: { pid: string }) {
36
50
  // defaulting to a live session with the super-agent so the chat works even on
37
51
  // a brand-new project with zero agents and zero conversations.
38
52
  const [selected, setSelected] = useState<ChatKey>(() => {
53
+ // An embedding screen that already knows which conversation the user
54
+ // picked wins over the URL — otherwise clicking a row in the inbox opens
55
+ // an empty new session and the message you clicked on is nowhere.
56
+ if (initialSelection) return initialSelection;
39
57
  const agent = params.get("agent");
40
58
  const conv = params.get("conv");
41
59
  const channel = params.get("channel");
@@ -183,7 +201,7 @@ export function ChatTab({ pid }: { pid: string }) {
183
201
 
184
202
  return (
185
203
  <div className="flex h-full overflow-hidden rounded-xl border border-border bg-card/40">
186
- <ChatList
204
+ {hideSidebar ? null : <ChatList
187
205
  pid={pid}
188
206
  agents={agentList}
189
207
  superAgentSlug={ROBY_SLUG}
@@ -191,7 +209,7 @@ export function ChatTab({ pid }: { pid: string }) {
191
209
  selected={selected}
192
210
  onSelect={selectChat}
193
211
  onNewChat={onNewChat}
194
- />
212
+ />}
195
213
 
196
214
  <section className="flex min-w-0 flex-1 flex-col">
197
215
  <header className="flex shrink-0 items-center justify-between gap-3 border-b border-border px-4 py-3">
@@ -5,6 +5,7 @@ import { Plus } from "lucide-react";
5
5
  import { Routines, type RoutineEntry } from "../../lib/api";
6
6
  import { Button, Dialog, Empty, Loading } from "../../components/ui";
7
7
  import { useToast } from "../../components/Toast";
8
+ import { Section } from "../../components/Section";
8
9
  import { t } from "../../i18n";
9
10
  import { RoutineList } from "../../components/routines/RoutineList";
10
11
  import { RoutineDetail } from "../../components/routines/RoutineDetail";
@@ -75,23 +76,24 @@ export function RoutinesTab({ pid }: { pid: string }) {
75
76
  };
76
77
 
77
78
  return (
78
- <div className="flex h-full min-h-0 flex-col gap-3">
79
- {/* header (title + new) */}
80
- <div className="flex shrink-0 items-start justify-between gap-4">
81
- <div>
82
- <h2 className="text-lg font-semibold tracking-tight">{t("project.routines.title")}</h2>
83
- <p className="mt-0.5 text-sm text-muted-fg">{t("project.routines.subtitle")}</p>
84
- </div>
79
+ // Same frame as every other list page. This one used to draw its own
80
+ // header outside a card while Tasks and Commitments sat inside one, so
81
+ // moving between them the whole page shifted.
82
+ <Section
83
+ fullHeight
84
+ title={t("project.routines.title")}
85
+ description={t("project.routines.subtitle")}
86
+ action={
85
87
  <Button size="sm" variant="primary" onClick={() => setEditing({ kind: "super_agent", schedule: "every:10m", enabled: true })}>
86
88
  <Plus size={14} /> {t("project.routines.new_btn")}
87
89
  </Button>
88
- </div>
89
-
90
+ }
91
+ >
90
92
  {list.isLoading && <Loading />}
91
93
  {!list.isLoading && rows.length === 0 && <Empty>{t("project.routines.empty")}</Empty>}
92
94
 
93
95
  {rows.length > 0 && (
94
- <div className="grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)] grid-cols-[minmax(200px,260px)_1fr] overflow-hidden rounded-xl border border-border bg-card/40">
96
+ <div className="grid min-h-0 flex-1 grid-rows-[minmax(0,1fr)] grid-cols-[minmax(200px,260px)_1fr] overflow-hidden rounded-lg border border-border">
95
97
  <RoutineList routines={rows} selectedName={selected?.name ?? null} onSelect={selectRoutine} />
96
98
  <div className="min-h-0 min-w-0 overflow-hidden">
97
99
  {selected
@@ -148,6 +150,6 @@ export function RoutinesTab({ pid }: { pid: string }) {
148
150
  >
149
151
  <p className="text-sm text-muted-fg">{t("project.routines.run_confirm_body")}</p>
150
152
  </Dialog>
151
- </div>
153
+ </Section>
152
154
  );
153
155
  }
@@ -55,7 +55,7 @@ export interface AgentDetail extends AgentEntry {
55
55
 
56
56
  export interface RoutineEntry {
57
57
  name: string;
58
- kind: "heartbeat" | "exec_agent" | "super_agent" | "telegram" | "shell";
58
+ kind: "heartbeat" | "exec_agent" | "super_agent" | "telegram" | "shell" | "watch";
59
59
  schedule: string;
60
60
  spec: Record<string, unknown>;
61
61
  enabled: boolean;
@@ -228,6 +228,15 @@ export interface ConversationMessage {
228
228
  actor_kind?: string;
229
229
  model?: string;
230
230
  usage?: ChatUsage;
231
+ /** Compact record of what the turn did, written at the time (the live tool
232
+ * events are long gone by the time a thread is read back). */
233
+ tool_summary?: ToolSummary;
234
+ }
235
+
236
+ export interface ToolSummary {
237
+ total: number;
238
+ failed: number;
239
+ tools: { name: string; count: number; failed: number }[];
231
240
  }
232
241
 
233
242
  export interface ConversationDetail {