Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions .github/workflows/build-tc-helper.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Compiles the tc_helper Rust native library from source (instead of relying on
# the pre-built .so committed under android/app/src/main/jniLibs/) and then
# builds the production APK against the freshly-compiled library.
#
# This is the "proper" fix for stale committed binaries: the .so is regenerated
# from rust/ on every run, so it can never drift from the Rust source. Once this
# is green, the committed jniLibs/*.so can be git-ignored and purged from history.
#
# NOTE: This workflow has not yet been run on CI — action versions, the NDK
# version, and the cargo-ndk invocation are transcribed from a verified LOCAL
# build (macOS) and may need small adjustments on the first CI run.
#
# Both arm64-v8a and armeabi-v7a are built. taskchampion is configured with only
# the server-sync backend (see rust/Cargo.toml), so there's no aws-lc-sys — hence
# no cmake/ninja/bindgen/libclang toolchain needed; ring builds with just the
# Rust + NDK toolchain.
name: Build tc_helper (compile from source)

on:
workflow_dispatch:
pull_request:
branches: [main, reports]
paths:
- "rust/**"
- "android/**"
- ".github/workflows/build-tc-helper.yml"

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Set persist-credentials: false on checkout.

The actions/checkout@v4 action persists the GITHUB_TOKEN in .git/config by default. Since this workflow runs on pull_request and checks out untrusted PR code, disabling credential persistence eliminates a potential token-exfiltration vector.

🛡️ Proposed fix
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.26.1)

[warning] 32-32: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tc-helper.yml at line 32, The checkout step in the
build-tc-helper workflow persists the GITHUB_TOKEN by default, which is unsafe
for pull_request runs that execute untrusted code. Update the existing
actions/checkout@v4 step to disable credential persistence by setting
persist-credentials to false so the workflow no longer leaves the token in git
config.

Source: Linters/SAST tools


- uses: actions/setup-java@v4
with:
distribution: "temurin"
java-version: "17"

# Android SDK + NDK. cargo-ndk reads ANDROID_NDK_HOME to find the toolchain.
- uses: android-actions/setup-android@v3
- name: Install NDK
run: |
sdkmanager "ndk;26.1.10909125"
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/26.1.10909125" >> "$GITHUB_ENV"

- name: Install Rust + Android targets + cargo-ndk
run: |
rustup toolchain install stable --profile minimal
rustup target add aarch64-linux-android armv7-linux-androideabi
cargo install cargo-ndk --version ^4

- name: Compile tc_helper (arm64-v8a + armeabi-v7a) from source
working-directory: rust
run: |
cargo ndk -t arm64-v8a -t armeabi-v7a \
-o ../android/app/src/main/jniLibs \
build --release

- name: Show freshly-built libs
run: ls -lhR android/app/src/main/jniLibs/

- uses: subosito/flutter-action@v2
with:
flutter-version: "3.44.5"

- run: flutter pub get
- run: flutter build apk --release --flavor production
Comment on lines +62 to +67

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What is the latest stable Flutter SDK version as of July 2026?

💡 Result:

As of July 9, 2026, the latest stable version of the Flutter SDK is 3.44.5 [1][2]. This version is part of the Flutter 3.44 release series, which was officially released in May 2026 [3]. You can ensure you are on the latest stable version by running the following commands in your terminal [4][2]: $ flutter channel stable $ flutter upgrade

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the workflow around the referenced lines
FILE=".github/workflows/build-tc-helper.yml"
wc -l "$FILE"
sed -n '1,140p' "$FILE" | nl -ba | sed -n '45,90p'

Repository: CCExtractor/taskwarrior-flutter

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/build-tc-helper.yml"

# Show the relevant section with line numbers using tools likely available in the sandbox
awk 'NR>=45 && NR<=75 { printf "%4d  %s\n", NR, $0 }' "$FILE"

Repository: CCExtractor/taskwarrior-flutter

Length of output: 1281


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/build-tc-helper.yml"

# Show the job header and early lines for timeout settings
awk 'NR>=1 && NR<=45 { printf "%4d  %s\n", NR, $0 }' "$FILE"

Repository: CCExtractor/taskwarrior-flutter

Length of output: 2111


Add an explicit timeout to the build job. The workflow has no timeout-minutes, so a hung Rust/Flutter build can burn runner minutes for hours; timeout-minutes: 45 is a reasonable guard.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/build-tc-helper.yml around lines 62 - 67, The build job in
the workflow needs an explicit timeout guard to prevent runaway Rust/Flutter
builds. Update the job that contains the flutter-action step and the flutter
build apk command to set a reasonable `timeout-minutes` value, using the
existing build job definition as the place to add it. Keep the change focused on
the workflow job configuration so the build is automatically terminated if it
hangs.


- uses: actions/upload-artifact@v4
with:
name: production-apk-from-source
path: build/app/outputs/flutter-apk/app-production-release.apk
Binary file not shown.
Binary file modified android/app/src/main/jniLibs/arm64-v8a/libtc_helper.so
Binary file not shown.
Binary file not shown.
Binary file modified android/app/src/main/jniLibs/armeabi-v7a/libtc_helper.so
Binary file not shown.
10 changes: 5 additions & 5 deletions ios/tc_helper.xcframework/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,32 @@
<key>BinaryPath</key>
<string>tc_helper.framework/tc_helper</string>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>tc_helper.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
<dict>
<key>BinaryPath</key>
<string>tc_helper.framework/tc_helper</string>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>tc_helper.framework</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
Expand Down
Binary file not shown.
Binary file not shown.
13 changes: 13 additions & 0 deletions lib/app/modules/home/controllers/home_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ class HomeController extends GetxController {
final RxSet<String> selectedTags = <String>{}.obs;
final RxList<Task> queriedTasks = <Task>[].obs;
final RxList<Task> searchedTasks = <Task>[].obs;
// Reactive mirror of the search box text. `searchedTasks` only drives the
// local-taskc list (TasksBuilder); the TaskChampion replica list reads
// `tasksFromReplica` directly, so it needs an observable query to rebuild and
// filter on each keystroke. Kept in sync by search()/toggleSearch().
final RxString searchQuery = ''.obs;
final RxList<DateTime?> selectedDates = List<DateTime?>.filled(4, null).obs;
final RxMap<String, TagMetadata> pendingTags = <String, TagMetadata>{}.obs;
final RxMap<String, ProjectNode> projects = <String, ProjectNode>{}.obs;
Expand Down Expand Up @@ -86,6 +91,12 @@ class HomeController extends GetxController {
taskdb = TaskDatabase();
taskdb.open();
getUniqueProjects();
// Initialize the pending/waiting filters from their persisted values.
// Without this the RxBool defaults to false, and the replica list view
// (which filters `status == pending` only when pendingFilter is true)
// shows completed tasks only — hiding every pending task on first load.
pendingFilter.value = Query(storage.tabs.tab()).getPendingFilter();
waitingFilter.value = Query(storage.tabs.tab()).getWaitingFilter();
_loadTaskChampion();
fetchTasksFromDB();

Expand Down Expand Up @@ -499,10 +510,12 @@ class HomeController extends GetxController {
if (!searchVisible.value) {
searchedTasks.assignAll(queriedTasks);
searchController.text = '';
searchQuery.value = '';
}
}

void search(String term) {
searchQuery.value = term;
searchedTasks.assignAll(
queriedTasks
.where(
Expand Down
25 changes: 22 additions & 3 deletions lib/app/modules/home/views/home_page_body.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,25 @@ class HomePageBody extends StatelessWidget {
child: Padding(
padding: const EdgeInsets.only(left: 8.0, right: 8.0),
child: Obx(
() => Column(
() {
// Read the replica list reactively here so the whole view rebuilds
// when tasks arrive. On launch, taskReplica flips true (triggering a
// rebuild) *before* the async FFI fetch populates tasksFromReplica;
// without a reactive read of the list itself, the populated tasks
// would never appear (the view stays on its initial empty build).
var replicaTasks = controller.tasksFromReplica.toList();
// Apply the search text to the replica list. Unlike the local-taskc
// path (which reads the pre-filtered `searchedTasks`), this builder
// gets the raw list, so filter here. Reading `searchQuery`/
// `searchVisible` inside the Obx makes it rebuild on each keystroke.
final query = controller.searchQuery.value.trim().toLowerCase();
if (controller.searchVisible.value && query.isNotEmpty) {
replicaTasks = replicaTasks
.where((task) =>
(task.description ?? '').toLowerCase().contains(query))
.toList();
}
return Column(
children: <Widget>[
if (controller.searchVisible.value)
Container(
Expand Down Expand Up @@ -134,14 +152,15 @@ class HomePageBody extends StatelessWidget {
child: Expanded(
child: Scrollbar(
child: TaskReplicaViewBuilder(
replicaTasks: controller.tasksFromReplica,
replicaTasks: replicaTasks,
pendingFilter: controller.pendingFilter.value,
selectedSort: controller.selectedSort.value,
project: controller.projectFilter.value,
),
)))
],
),
);
},
),
),
),
Expand Down
9 changes: 5 additions & 4 deletions lib/app/modules/home/views/show_tasks_replica.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@ class TaskReplicaViewBuilder extends StatelessWidget {
TaskwarriorColorTheme tColors =
Theme.of(context).extension<TaskwarriorColorTheme>()!;

return Obx(() {
List<TaskForReplica> tasks = List<TaskForReplica>.from(replicaTasks);
if (project != null && project != 'All Projects') {
// Reactivity is handled by the parent Obx in home_page_body (which reads
// tasksFromReplica); this widget just renders the snapshot it's given, so
// it must NOT be an Obx (an Obx with no observable read throws ObxError).
List<TaskForReplica> tasks = List<TaskForReplica>.from(replicaTasks);
if (project != null && project!.isNotEmpty && project != 'All Projects') {
tasks = tasks.where((task) => task.project == project).toList();
}
tasks.sort((a, b) {
Expand Down Expand Up @@ -203,7 +205,6 @@ class TaskReplicaViewBuilder extends StatelessWidget {
},
),
);
});
}

Color _getPriorityColor(String priority) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'package:taskwarrior/app/models/storage/set_config.dart';
import 'package:taskwarrior/app/modules/home/controllers/home_controller.dart';
import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart';
import 'package:taskwarrior/app/tour/manage_task_server_page_tour.dart';
import 'package:taskwarrior/app/tour/safe_tour.dart';
import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart';
import 'package:taskwarrior/app/utils/home_path/home_path.dart' as rc;
import 'package:taskwarrior/app/utils/taskserver/taskserver.dart';
Expand Down Expand Up @@ -221,18 +222,20 @@ class ManageTaskServerController extends GetxController {
void showManageTaskServerPageTour(BuildContext context) {
Future.delayed(
const Duration(milliseconds: 500),
() {
SaveTourStatus.getManageTaskServerTourStatus().then((value) => {
if (value == false)
{
tutorialCoachMark.show(context: context),
}
else
{
// ignore: avoid_print
print('User has seen this page'),
}
});
() async {
final seen = await SaveTourStatus.getManageTaskServerTourStatus();
if (seen) return;
await safeShowTour(
tutorialCoachMark: tutorialCoachMark,
context: context,
targetKeys: [
configureTaskRC,
configureServerCertificate,
configureTaskServerKey,
configureYourCertificate,
],
markSeen: () => SaveTourStatus.saveManageTaskServerTourStatus(true),
);
},
);
}
Expand Down
26 changes: 14 additions & 12 deletions lib/app/modules/profile/controllers/profile_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:taskwarrior/app/modules/splash/controllers/splash_controller.dart';
import 'package:taskwarrior/app/tour/profile_page_tour.dart';
import 'package:taskwarrior/app/tour/safe_tour.dart';
import 'package:taskwarrior/app/utils/constants/taskwarrior_colors.dart';
import 'package:taskwarrior/app/utils/app_settings/app_settings.dart';
import 'package:tutorial_coach_mark/tutorial_coach_mark.dart';
Expand Down Expand Up @@ -45,18 +46,19 @@ class ProfileController extends GetxController {
void showProfilePageTour(BuildContext context) {
Future.delayed(
const Duration(milliseconds: 500),
() {
SaveTourStatus.getProfileTourStatus().then((value) => {
if (value == false)
{
tutorialCoachMark.show(context: context),
}
else
{
// ignore: avoid_print
print('User has seen this page'),
}
});
() async {
final seen = await SaveTourStatus.getProfileTourStatus();
if (seen) return;
await safeShowTour(
tutorialCoachMark: tutorialCoachMark,
context: context,
targetKeys: [
currentProfileKey,
addNewProfileKey,
manageSelectedProfileKey,
],
markSeen: () => SaveTourStatus.saveProfileTourStatus(true),
);
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ class TaskcDetailsController extends GetxController {
late RxString rtype;
late RxString recur;
late RxList<Annotation> annotations;
// Blocking state surfaced by the Rust serializer (replica tasks only).
late RxBool isBlocked;
late RxBool isBlocking;
late RxList<String> previousTags = <String>[].obs;

@override
Expand Down Expand Up @@ -64,6 +67,8 @@ class TaskcDetailsController extends GetxController {
rtype = "".obs;
recur = "".obs;
annotations = <Annotation>[].obs;
isBlocked = false.obs;
isBlocking = false.obs;
} else if (task is TaskForReplica) {
description = (task.description ?? '').obs;
project = (task.project ?? 'None').obs;
Expand All @@ -81,10 +86,13 @@ class TaskcDetailsController extends GetxController {
? task.tags!.map((e) => e.toString()).toList().obs
: <String>[].obs;
previousTags = tags.toList().obs;
depends = "".split(",").obs;
// Attributes now surfaced by the Rust serializer.
depends = (task.depends ?? <String>[]).obs;
rtype = "".obs;
recur = "".obs;
annotations = <Annotation>[].obs;
recur = (task.recur ?? "").obs;
annotations = (task.annotations ?? <Annotation>[]).obs;
isBlocked = (task.isBlocked ?? false).obs;
isBlocking = (task.isBlocking ?? false).obs;
} else {
// Fallback
description = ''.obs;
Expand All @@ -100,6 +108,8 @@ class TaskcDetailsController extends GetxController {
rtype = "".obs;
recur = "".obs;
annotations = <Annotation>[].obs;
isBlocked = false.obs;
isBlocking = false.obs;
}
}

Expand Down
39 changes: 39 additions & 0 deletions lib/app/modules/taskc_details/views/taskc_details_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,45 @@ class TaskcDetailsView extends GetView<TaskcDetailsController> {
controller.tags.join(', '),
(value) => controller.updateListField(controller.tags, value),
),
// Attributes surfaced by the enriched Rust serializer (D2).
// Replica tasks only; read-only (the mobile UI cannot edit
// dependencies/annotations yet).
if (controller.isReplicaTask) ...[
_buildDetail(
context,
'Blocked:',
controller.isBlocked.value ? 'Yes' : 'No',
),
_buildDetail(
context,
'Blocking:',
controller.isBlocking.value ? 'Yes' : 'No',
),
_buildDetail(
context,
'Depends:',
controller.depends.isEmpty
? 'None'
: controller.depends.join(', '),
),
_buildDetail(
context,
'Recur:',
controller.recur.value.isEmpty
? 'None'
: controller.recur.value,
),
_buildDetail(
context,
'Annotations:',
controller.annotations.isEmpty
? 'None'
: controller.annotations
.map((a) =>
'• ${a.description ?? ''}${(a.entry != null && a.entry!.isNotEmpty) ? ' (${a.entry})' : ''}')
.join('\n'),
),
],
if (controller.isLocalTask) ...[
_buildDetail(
context,
Expand Down
Loading